trophy-ui
uiachievement-cardsource: ok · refreshed
Achievement Card
React achievement card with totals, featured badges, and scrollable lists for profile and progress surfaces. Fits achievement UI examples in SaaS, consumer and community products. Open source, built on shadcn/ui + Tailwind.
npx shadcn add "https://ui.trophy.so/r/achievement-card.json"free
Where it goes
profilefeature-grid
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightRegistry dependencies
achievement-badgeachievement-listbutton
Files1 · source cached 10/8/2026, 7:41:22 AM
registry/trophy/ui/achievement-card.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { AchievementBadge } from "@/components/ui/achievement-badge"
import { AchievementList } from "@/components/ui/achievement-list"
import { Button } from "@/components/ui/button"
interface Achievement {
id: string
name: string
description?: string | null
trigger: "metric" | "api" | "streak"
badgeUrl?: string | null
progress?: number
rarity?: number
}
interface UserAchievement extends Achievement {
achievedAt: string | null
}
interface AchievementCardProps extends React.HTMLAttributes<HTMLDivElement> {
achievements: UserAchievement[]
highlightedAchievements: UserAchievement[]
badgeSize?: "sm" | "default" | "lg"
lockedStyle?: "grayscale" | "silhouette" | "hidden"
onAchievementClick?: (achievement: UserAchievement) => void
}
const AchievementCard = React.forwardRef<HTMLDivElement, AchievementCardProps>(
(
{
className,
achievements,
highlightedAchievements,
badgeSize = "default",
lockedStyle = "grayscale",
onAchievementClick,
...props
},
ref
) => {
const unlockedCount = achievements.filter(
(a) => a.achievedAt !== null
).length
return (
<div
ref={ref}
className={cn("bg-card rounded-2xl border p-6 shadow-sm", className)}
{...props}
>
<div className="text-center">
<p className="text-7xl font-bold tracking-tight sm:text-8xl">
{unlockedCount}
</p>
<p className="text-muted-foreground mt-1 text-sm font-medium">
Badges Unlocked
</p>
</div>
<div className="mt-10 flex items-end justify-center gap-4">
{highlightedAchievements.slice(0, 3).map((achievement, index) => (
<AchievementBadge
key={achievement.id}
achievement={achievement}
badgeSize="sm"
onAchievementClick={onAchievementClick}
className={cn(
"w-28 border-0 bg-transparent p-0 shadow-none hover:shadow-none",
index === 1 ? "-translate-y-2" : "translate-y-1"
)}
/>
))}
</div>
<div className="mt-10">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-primary text-sm font-medium">
All Achievements
</h3>
<Button variant="link" size="sm" onClick={() => {}}>
See all
</Button>
</div>
<AchievementList
achievements={achievements}
badgeSize={badgeSize}
lockedStyle={lockedStyle}
onAchievementClick={onAchievementClick}
/>
</div>
</div>
)
}
)
AchievementCard.displayName = "AchievementCard"
export { AchievementCard }
export type { AchievementCardProps, Achievement, UserAchievement }