Achievement Badge
trophy/achievement-badgeFreeComponent
Open source React achievement badge component with locked/unlocked states, progress rings and rarity support. Use for achievement UI design and collections. Built on shadcn/ui + Tailwind.
Live preview
live · rendered by the registry
Rendered by trophy on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ui.trophy.so/r/achievement-badge.jsonSource
1"use client"23import * as React from "react"4import { Trophy } from "lucide-react"56import { cn } from "@/lib/utils"78interface Achievement {9 id: string10 name: string11 trigger: "metric" | "api" | "streak"12 badgeUrl?: string | null13 progress?: number14 rarity?: number15}1617interface UserAchievement extends Achievement {18 /** ISO date when earned, or `null` if locked */19 achievedAt: string | null20}2122interface AchievementBadgeProps extends React.HTMLAttributes<HTMLDivElement> {23 achievement: UserAchievement24 badgeSize?: "sm" | "default" | "lg" | "xl"25 onAchievementClick?: (achievement: UserAchievement) => void26}2728const badgeSizeMap = {29 sm: "h-12 w-12",30 default: "h-16 w-16",31 lg: "h-20 w-20",32 xl: "h-28 w-28",33} as const3435const iconSizeMap = {36 sm: "h-8 w-8",37 default: "h-10 w-10",38 lg: "h-12 w-12",39 xl: "h-16 w-16",40} as const4142const progressRingSizeMap = {43 sm: 72,44 default: 88,45 lg: 104,46 xl: 136,47} as const4849const AchievementBadge = React.forwardRef<50 HTMLDivElement,51 AchievementBadgeProps52>(53 (54 {55 className,56 achievement,57 badgeSize = "default",58 onAchievementClick,59 ...props60 },61 ref62 ) => {63 const isUnlocked = achievement.achievedAt !== null6465 const hasProgress = isUnlocked && typeof achievement.progress === "number"66 const progress = hasProgress67 ? Math.min(100, Math.max(0, achievement.progress ?? 0))68 : 069 const hasRarity = typeof achievement.rarity === "number"70 const rarity = hasRarity71 ? Math.min(100, Math.max(1, Math.round(achievement.rarity ?? 1)))72 : null73 const ringSize = progressRingSizeMap[badgeSize]74 const ringStrokeWidth = 475 const ringRadius = (ringSize - ringStrokeWidth) / 276 const ringCircumference = 2 * Math.PI * ringRadius77 const ringDashoffset =78 ringCircumference - (progress / 100) * ringCircumference7980 const statusLabel = isUnlocked ? "Earned" : "Locked"81 const itemLabel = `${achievement.name} - ${statusLabel}`8283 return (84 <div85 ref={ref}86 role={onAchievementClick ? "button" : "listitem"}87 aria-label={onAchievementClick ? itemLabel : undefined}88 tabIndex={onAchievementClick ? 0 : undefined}89 onClick={() => onAchievementClick?.(achievement)}90 onKeyDown={91 onAchievementClick92 ? (e) => {93 if (e.key === "Enter" || e.key === " ") {94 e.preventDefault()95 onAchievementClick(achievement)96// … truncated
What it pulls in
npm packages
Files it writes
More from trophy
All 18 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Achievement Cardachievement-card | trophy | Components | Free | no deps | |
| Achievement Gridachievement-grid | trophy | Components | Free | 2 deps | |
| Achievement Listachievement-list | trophy | Components | Free | 2 deps | |
| Achievement Unlockedachievement-unlocked | trophy | Components | Free | 1 dep | |
| Leaderboard Cardleaderboard-card | trophy | Components | Free | no deps | |
| Leaderboard Podiumleaderboard-podium | trophy | Components | Free | 2 deps | |
| Leaderboard Rankingsleaderboard-rankings | trophy | Components | Free | 2 deps | |
| Points Awardspoints-awards | trophy | Components | Free | 2 deps |
