# Profile Card

> Profile card with a gradient cover, avatar, verified badge, tags, stats and a follow toggle.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/profile-card
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/profile-card.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/profile-card-demo.tsx"
"use client"

import { ProfileCard } from "@/components/profile-card"

export default function ProfileCardDemo() {
  return (
    <div className="flex flex-wrap items-start justify-center gap-6">
      <ProfileCard
        name="Maya Chen"
        handle="mayachen"
        verified
        bio="Product designer. Building calm interfaces for busy people."
        location="Shanghai, CN"
        tags={["Design systems", "Motion", "Accessibility"]}
        stats={[
          { label: "Posts", value: 248 },
          { label: "Followers", value: 12840 },
          { label: "Following", value: 312 },
        ]}
      />
      <ProfileCard
        name="Jonas Weber"
        handle="jonasw"
        bio="Frontend engineer. Opinions on bundle size."
        location="Berlin, DE"
        cover="linear-gradient(135deg, #0ea5e9, #22c55e)"
        defaultFollowing
        stats={[
          { label: "Posts", value: 96 },
          { label: "Followers", value: 980 },
          { label: "Following", value: 140 },
        ]}
      />
    </div>
  )
}
```

## Source

```tsx title="components/profile-card.tsx"
"use client"

import * as React from "react"
import { BadgeCheck, MapPin, UserCheck, UserPlus } from "lucide-react"

import { cn } from "@/lib/utils"

export interface ProfileStat {
  label: string
  value: number
}

export interface ProfileCardProps extends Omit<React.ComponentProps<"div">, "onChange"> {
  name: string
  handle: string
  bio?: string
  location?: string
  verified?: boolean
  tags?: string[]
  stats?: ProfileStat[]
  /** CSS gradient for the cover */
  cover?: string
  defaultFollowing?: boolean
  onFollowChange?: (following: boolean) => void
}

const compact = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, "")}k` : String(n))

export function ProfileCard({
  name,
  handle,
  bio,
  location,
  verified = false,
  tags = [],
  stats = [],
  cover = "linear-gradient(135deg, #6366f1, #ec4899 55%, #f59e0b)",
  defaultFollowing = false,
  onFollowChange,
  className,
  ...props
}: ProfileCardProps) {
  const [following, setFollowing] = React.useState(defaultFollowing)
  const followersIndex = stats.findIndex((s) => /follower/i.test(s.label))

  return (
    <div {...props} className={cn("w-full max-w-xs overflow-hidden rounded-3xl border bg-card text-card-foreground shadow-sm", className)}>
      <div className="h-24" style={{ backgroundImage: cover }} />
      <div className="px-5 pb-5">
        <div className="-mt-10 flex items-end justify-between">
          <span
            aria-hidden
            className="flex size-20 items-center justify-center rounded-full border-4 border-card bg-foreground text-2xl font-semibold text-background"
          >
            {name
              .split(" ")
              .map((part) => part[0])
              .slice(0, 2)
              .join("")}
          </span>
          <button
            type="button"
            aria-pressed={following}
            onClick={() => {
              setFollowing(!following)
              onFollowChange?.(!following)
            }}
            className={cn(
              "inline-flex h-9 items-center gap-1.5 rounded-full px-4 text-sm font-medium transition-colors",
              following ? "border bg-background hover:border-destructive hover:text-destructive" : "bg-primary text-primary-foreground hover:bg-primary/90",
            )}
          >
            {following ? <UserCheck className="size-4" /> : <UserPlus className="size-4" />}
            {following ? "Following" : "Follow"}
          </button>
        </div>

        <h3 className="mt-3 flex items-center gap-1 text-lg font-semibold leading-tight">
          {name}
          {verified && <BadgeCheck aria-label="Verified" className="size-4.5 fill-sky-500 text-background" />}
        </h3>
        <p className="text-sm text-muted-foreground">@{handle}</p>
        {bio && <p className="mt-3 text-sm leading-relaxed">{bio}</p>}
        {location && (
          <p className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
            <MapPin className="size-3.5" />
            {location}
          </p>
        )}
        {tags.length > 0 && (
          <ul className="mt-3 flex flex-wrap gap-1.5">
            {tags.map((tag) => (
              <li key={tag} className="rounded-full bg-secondary px-2.5 py-0.5 text-xs text-secondary-foreground">
                {tag}
              </li>
            ))}
          </ul>
        )}
        {stats.length > 0 && (
          <dl className="mt-4 grid grid-flow-col auto-cols-fr divide-x rounded-xl border text-center">
            {stats.map((stat, index) => (
              <div key={stat.label} className="px-2 py-2.5">
                <dd className="text-base font-semibold tabular-nums">{compact(stat.value + (index === followersIndex && following ? 1 : 0))}</dd>
                <dt className="text-xs text-muted-foreground">{stat.label}</dt>
              </div>
            ))}
          </dl>
        )}
      </div>
    </div>
  )
}
```
