Unitpost

Brand & interface system

Components

The kit, every variant, both themes

Everything here is imported from @/components/ui (packages/ui). Variant lists are checked against the components' own types, so this page can't describe a variant that doesn't exist. When to use what: DESIGN.md §3.

Button

One default-filled button per view. Icon-only buttons take an aria-label and a Hint. Links that look like buttons use render={<Link/>}.

variant
<Button variant="default | outline | secondary | ghost | destructive | link">
size
<Button size="xs | sm | default | lg">
icon sizesaria-label + Hint required
<Hint label="Edit"><Button size="icon-sm" variant="ghost" aria-label="Edit"><Pencil /></Button></Hint>
Read the docs
with HoverArrow / loading / disabled / as link
<Button className="group">Continue <HoverArrow /></Button>
<Button disabled><Spinner /> Saving…</Button>
<Button render={<Link href="/docs" />} variant="outline">Docs</Button>
USD
SplitButton · ButtonGroupone primary, the rest in the menu
<SplitButton menuLabel="More contact actions" menu={<>
  <DropdownMenuItem>Import CSV</DropdownMenuItem>
  <DropdownMenuItem>Export CSV</DropdownMenuItem>
</>}>
  <Button>Add contact <Plus /></Button>
</SplitButton>

<ButtonGroup><ButtonGroupText>USD</ButtonGroupText><Button variant="outline">Set budget</Button></ButtonGroup>
Paginationfirst · prev · steps · next · last; current page is outline
<Pagination><PaginationContent>
  <PaginationItem><PaginationFirst href="#" /></PaginationItem>
  <PaginationItem><PaginationPrevious href="#" /></PaginationItem>
  {pageRange(5, 20).map((p, i) => p === null
    ? <PaginationItem key={i}><PaginationEllipsis /></PaginationItem>
    : <PaginationItem key={p}><PaginationLink href="#" isActive={p === 5}>{p}</PaginationLink></PaginationItem>)}
  <PaginationItem><PaginationNext href="#" /></PaginationItem>
  <PaginationItem><PaginationLast href="#" /></PaginationItem>
</PaginationContent></Pagination>

Toggle · ToggleGroup

Pressed-state controls: formatting, filter chips, view switches. ToggleGroup for one-of-N (or many-of-N) in a row.

Toggle
<Toggle variant="default | outline" size="sm | default | lg" aria-label="Bold"><Bold /></Toggle>
ToggleGroup
<ToggleGroup value={align} onValueChange={setAlign}>
  <ToggleGroupItem value="left">Left</ToggleGroupItem> …

Fields

Every field is h-8, pill, bg-surface-subtle, hover border-strong, focus ring-ring/30. Label above; helper below; aria-invalid turns the ring red.

Shown in the recipient's inbox.

Input · Label · Field
<Field>
  <FieldLabel htmlFor="name">Sender name</FieldLabel>
  <Input id="name" placeholder="Acme" />
  <FieldDescription>Shown in the inbox.</FieldDescription>
</Field>
⌘K
https://
InputGroupsearch, prefix, copyable value
<InputGroup>
  <InputGroupAddon><Search /></InputGroupAddon>
  <InputGroupInput placeholder="Search…" />
</InputGroup>
Textarea
<Textarea placeholder="…" rows={3} />
Select≤ ~30 known options
<Select items={{ eu: "EU (Frankfurt)", us: "US (Virginia)" }} defaultValue="eu">
  <SelectTrigger><SelectValue /></SelectTrigger>
  <SelectContent>
    <SelectGroup><SelectLabel>Region</SelectLabel>
      <SelectItem value="eu">EU (Frankfurt)</SelectItem> …
Comboboxsearchable list
<Combobox items={items}>
  <ComboboxInput placeholder="Pick a template" />
  <ComboboxContent><ComboboxList>{(item) => <ComboboxItem key={item} value={item}>{item}</ComboboxItem>}</ComboboxList></ComboboxContent>
</Combobox>
Checkbox · RadioGroup · Switch
<Checkbox id="c" /> <Label htmlFor="c">…</Label>
<RadioGroup defaultValue="a"><RadioGroupItem value="a" id="a" /> …
<Switch size="sm | default" />
Slider
<Slider defaultValue={40} min={0} max={100} />

Slide to confirm

Slide to confirm (ConfirmDialog slide)Drag to 100% to arm the destructive button; release early and it snaps back. Only for irreversible, protected, or bulk-destructive actions (the staff override on the protected system workspace) — never ordinary deletes.
<ConfirmDialog slide slideTestid="protected-override-slider" confirmLabel="Delete 3 contacts" … />

Card

filled for information, outline for selectable objects / tables / previews, raised above a filled region. tone for status. render for link tiles. Never nested.

appearance="filled"
CardHeader › CardTitle + CardDescription + CardAction
CardContent carries the body. Spacing comes from --card-spacing (size="sm" tightens it).
appearance="outline"
CardHeader › CardTitle + CardDescription + CardAction
CardContent carries the body. Spacing comes from --card-spacing (size="sm" tightens it).
appearance="raised"
CardHeader › CardTitle + CardDescription + CardAction
CardContent carries the body. Spacing comes from --card-spacing (size="sm" tightens it).
appearance="ghost"
CardHeader › CardTitle + CardDescription + CardAction
CardContent carries the body. Spacing comes from --card-spacing (size="sm" tightens it).
Link tile · render={<Link />}
The whole card is the link. Hover uses surface-hover.

Tabs

variant="default" is the pill segmented control (views, sections); variant="line" is the underline (filters over a list). Route tabs: SegmentedTabLinks; state tabs: SegmentedTabs.

Everything.
variant="default"
<Tabs defaultValue="all"><TabsList><TabsTrigger value="all">All</TabsTrigger>…</TabsList><TabsContent value="all">…</TabsContent></Tabs>
1,204 sent.
variant="line"
<TabsList variant="line">

Table

Rows hover surface-hover, selected surface-active. density="compact" for lookup tables. Numeric columns right-aligned with their header. Frame it with an outline Card or rounded-xl ring.

DomainStatusSent
acme.comVerified12,757
mail.acme.comPending0
old.acme.coFailed—
density="default"
DomainStatusSent
acme.comVerified12,757
mail.acme.comPending0
old.acme.coFailed—
density="compact"

Overlays

Dialog for tasks, AlertDialog for confirmations (ConfirmDialog wraps it), Sheet for side panels, Popover for anchored controls, DropdownMenu for object actions, Tooltip (via Hint) for text only.

Dialog sizessm — confirmations and single-field prompts · md (default) — a form of 2–6 fields · lg — a form with a table/list, or two columns · xl — editors, previews, multi-column pickers. Width comes from size only; ui:check fails a max-w-* className.
<DialogContent size="sm">…</DialogContent>   // confirmation
<DialogContent>…</DialogContent>              // md — the form default
<DialogContent size="lg">…</DialogContent>   // form + table / two columns
<DialogContent size="xl">…</DialogContent>   // editor / preview
Dialog · AlertDialog · Sheet · Popover · DropdownMenu · TooltipDialog for tasks (sized by the scale above); AlertDialog is the confirmation primitive — ConfirmDialog wraps it (simple, typed, or slide-to-confirm).
<Dialog><DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger><DialogContent>…</DialogContent></Dialog>

Status & feedback

Badge for status chips. Alert for persistent in-context notices. Skeleton/Spinner/Progress for pending. Empty and error regions use EmptyState / ErrorState (see Patterns).

defaultsecondaryoutlineghostsuccesswarningdestructiveinfobrandtealvioletlink
Badge
<Badge variant="default | secondary | outline | ghost | success | warning | destructive | info | brand | teal | violet | link">
neutral: an inline one-liner (size="sm").
info: an inline one-liner (size="sm").
success: an inline one-liner (size="sm").
warning: an inline one-liner (size="sm").
destructive: an inline one-liner (size="sm").
DNS records not found
We looked up mail.acme.com and none of the three records resolved yet. Propagation can take up to 48 hours.
Looking for DNS records…
This usually takes under a minute.
Alert
<Alert variant="neutral | info | success | warning | destructive" size="sm | default" title="…" action={<Button variant="link" size="xs" />}>
Sending…
Plan usage
x
Contacts
x
Skeleton · Spinner · Progress
<Skeleton className="h-4 w-40" />  <Spinner />  <Progress value={64} tone="warning"><ProgressLabel>Plan usage</ProgressLabel><ProgressValue /></Progress>
MV
AB
+3
⌘⏎
  1. Verify domain

  2. Add sender

  3. Send a test

Every accepted message.
No.
Avatar · Kbd · Breadcrumb · Separator · Steps · Accordion

Interaction states

Hover, pressed, selected, focus-visible, disabled — all from the kit. Tab through this row to see focus rings.

color + icon + word, never color alone
Neutral controls