bottega

cards

Profile User Card

A social-style profile card: a full-width token-gradient cover banner (linear-gradient --primary → --accent, replacing the original hardcoded #FC726E SVG), a circular initials-avatar disc that overlaps the banner/body seam, a name + handle below, and a Follow (primary) + Message (ghost) action row. Both buttons are keyboard-operable with focus-visible rings. Fully server-renderable — no hooks or JS state required. Light/dark-robust by construction: every color comes from tokens.

Ported from andrew-demchenk0 (MIT)

Alex Rivera

@alexrivera


Install

1. Register the namespace (once per project):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/profile-user-card

Usage

Usagetsx
<ProfileUserCard />

Props

PropTypeDefaultDescription
namestringAlex RiveraDisplay name shown below the avatar.
handlestring@alexriveraHandle or role shown below the name (e.g. "@alexrivera" or "Web Developer").
initialsstringARUp to 2-character initials rendered on the avatar disc.
primaryLabelstringFollowLabel for the primary action button (default "Follow").
ghostLabelstringMessageLabel for the ghost action button (default "Message").
classNamestringAdditional class names merged onto the root element.

Source

profile-user-card.tsxtsx
import styles from "./profile-user-card.module.css";

export interface ProfileUserCardProps {
  /** Display name shown below the avatar. */
  name?: string;
  /** Handle or role shown below the name (e.g. "@alexrivera" or "Web Developer"). */
  handle?: string;
  /** Up to 2-character initials rendered on the avatar disc. */
  initials?: string;
  /** Label for the primary action button (default "Follow"). */
  primaryLabel?: string;
  /** Label for the ghost action button (default "Message"). */
  ghostLabel?: string;
  /** Additional class names merged onto the root element. */
  className?: string;
}

/**
 * ProfileUserCard — social-style profile card with a token-gradient cover
 * banner, a circular initials avatar overlapping the banner/body seam,
 * a name + handle, and Follow/Message action buttons.
 *
 * Port of: https://uiverse.io/andrew-demchenk0/chatty-hound-50 (MIT, andrew-demchenk0)
 * Token map: --main-color → --foreground; --bg-color → --background;
 *            --submain-color → color-mix(oklch, --foreground 60%, --background);
 *            hardcoded #FC726E SVG gradient → linear-gradient(--primary → --accent)
 */
export function ProfileUserCard({
  name = "Alex Rivera",
  handle = "@alexrivera",
  initials = "AR",
  primaryLabel = "Follow",
  ghostLabel = "Message",
  className = "",
}: ProfileUserCardProps) {
  const safeInitials = initials.slice(0, 2).toUpperCase();

  return (
    <article
      className={[styles.card, className].filter(Boolean).join(" ")}
      aria-label={`Profile card for ${name}`}
    >
      {/* Cover banner — token gradient replaces hardcoded #FC726E SVG */}
      <div className={styles.banner} aria-hidden="true" />

      {/* Avatar ring + initials disc; aria-hidden — name below carries the meaning */}
      <div className={styles.avatarRing} aria-hidden="true">
        <div className={styles.initialsDisc}>{safeInitials}</div>
      </div>

      {/* Name */}
      <h3 className={styles.name}>{name}</h3>

      {/* Handle / subtitle */}
      <p className={styles.handle}>{handle}</p>

      {/* Action buttons — both keyboard-operable with focus-visible rings */}
      <div className={styles.actions}>
        <button type="button" className={styles.btnGhost}>
          {ghostLabel}
        </button>
        <button type="button" className={styles.btnPrimary}>
          {primaryLabel}
        </button>
      </div>
    </article>
  );
}

Dependencies

  • @bottega/tokens