← ui

person

lib

a person defined once: name, handle, picture and colour, the same everywhere

npx shadcn add @ag/person
Already installed? Preview an update
npx shadcn add @ag/person --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-10-10
  • New: short, the name where room is short (a caption's speaker, a chip, a mention in running text), and shortName(person), which falls back to name.
Updated 2026-10-10 · release notes

also called user, profile

shelf foundations

used by people-picker, chat, macos, tweet, telegram-chat

one person

handed to every surface

AAda@ada

the demo above, verbatim

person.demo.tsx
import { Sample } from "@/app/samples"
import type { Person } from "@/registry/base-nova/lib/person"

// #region person
const ADA: Person = {
  name: "Ada",
  handle: "@ada",
}
// #endregion

/** One person, defined once: the name and handle every surface that shows them reads
 *  (a chat's header, a Mac's lock screen). Without a picture, a surface draws its own
 *  placeholder, here the initial. */
export default function PersonDemo() {
  return (
    <Sample
      name="person"
      glance
      title="one person"
      description="handed to every surface"
      with="person"
    >
      <div className="flex items-center gap-3">
        <span className="flex size-10 items-center justify-center rounded-full bg-foreground/10 font-semibold">
          {ADA.name[0]}
        </span>
        <span className="leading-tight">
          <span className="block font-medium">{ADA.name}</span>
          <span className="block text-muted-foreground text-xs">
            {ADA.handle}
          </span>
        </span>
      </div>
    </Sample>
  )
}

the source, every file an install writes

person.ts
// A person, as every surface that shows one needs them: a chat's contact, a Mac's
// lock-screen account. Defined once by the page and handed to each component, so the
// same person wears the same name and picture everywhere. A surface with more to say
// about someone extends this (telegram-chat's `ChatProfile` adds `bot`).
// Framework-free.

export interface Person {
  /** The display name, in full: "John F. Kennedy". */
  name: string
  /** The name where room is short (a caption's speaker, a chip, a mention in running
   *  text): "Kennedy". Absent, `name`. */
  short?: string
  /** "@adriangalilea": what a mention types and a sub-line shows. */
  handle?: string
  /** The picture's URL. Absent, a surface draws its own placeholder. */
  avatar?: string
  /** An animated picture (mp4) that loops muted over `avatar`, where a surface
   *  animates one. */
  avatarVideo?: string
  /** The person's own colour, any CSS colour: what a surface that tells people apart
   *  by colour tints them with (a transcript's speaker). Absent, the surface picks one
   *  from its palette by order of appearance, so a page that never sets it still gets
   *  distinct people, and one that does keeps the same colour on every surface. */
  color?: string
}

/** The name to draw where room is short. */
export const shortName = (p: Person): string => p.short ?? p.name

/** Distinct, mid-light hues that read on a light page and a dark one alike: the
 *  palette a surface picks from for a person who states no `color`. One palette, so a
 *  chat and a transcript of the same people tint them the same. */
export const PERSON_PALETTE = [
  "oklch(0.64 0.15 250)",
  "oklch(0.66 0.15 40)",
  "oklch(0.64 0.14 150)",
  "oklch(0.62 0.17 320)",
  "oklch(0.70 0.13 90)",
  "oklch(0.64 0.12 200)",
] as const

/** Each key's colour: its person's own, else the palette's by the key's first place in
 *  `order` (the order people first appear in, so the first always wears the first hue).
 *  Repeats in `order` are fine; only the first place counts. */
export function personColors(
  order: readonly string[],
  people: Record<string, Person | undefined>,
): Record<string, string> {
  const out: Record<string, string> = {}
  ;[...new Set(order)].forEach((key, i) => {
    out[key] =
      people[key]?.color ??
      (PERSON_PALETTE[i % PERSON_PALETTE.length] as string)
  })
  return out
}