Skip to content
Brand guide

Chapter 11

Components

The library, component by component. Live examples for all of these are in the component gallery; the reference implementations are in components/ui/ and can be copied into sincelabs/app as-is.

11.1 Button

The one action control.

Variants

VariantFillTextUse
primaryaccent-ink #A34A1FwhiteThe one action the page exists for. One per view
secondarywhite, border-strongink-bodyEverything alongside the primary
ghostnoneink-mutedTertiary controls in dense chrome
destructiveerror-bg, error-dot bordererror-textOnly where something is actually destroyed

Sizessm 32px (table rows, toolbars), md 38px (inline chrome), lg 48px (forms, auth, marketing — the default).

Rules

  • The label is a verb phrase completing "I want to…": Save changes, Send invite, Delete organization. Never OK, Submit, Yes, Learn more.
  • Primary fill is accent-ink, not accent. White on #C4602A is 4.16:1 and fails AA for a 14px label; on #A34A1F it is 5.90:1.
  • type="submit" is required inside a form. The component defaults to type="button" and dies silently otherwise.
  • Disabled: 50% opacity, cursor-not-allowed. Keep it in the DOM and focusable-adjacent — do not remove it.
  • A link that looks like a button stays an <a>. Use buttonVariants().
  • Icon-only buttons need aria-label.

11.2 Card and Panel

Card is a bare container: white, border, 2xl radius, raised shadow.

Panel adds a titled header rail — a heading, an optional description, and right-aligned actions. Prefer Panel. Content that is worth boxing is almost always worth naming.

headingLevel is caller-set (h2 by default) so the page's heading outline stays correct.

Never nest a Panel inside a Panel. Two borders one inside the other is a signal that the page needs splitting.


11.3 Input

  • The label is always rendered and tied by id. A placeholder is not a label.
  • The placeholder shows a format[email protected] — not a repeat of the label.
  • hint for standing guidance; error replaces it when set.
  • aria-invalid and aria-describedby are wired automatically.
  • 48px, 10px radius, border-strong, white.
  • Focus: the 2px accent ring at 2px offset. Never outline: none.

The same rules govern textareas and selects. A textarea is border-strong, 9px radius, 3 rows minimum, and resizes vertically only.


11.4 StatusChip

State as a pill: background, text colour, and a dot.

Six tones — success, attention, warning, error, info, neutral. Pass a raw status string and toneForStatus() maps it, or pass tone directly.

The mapping lives in one place so pending cannot render as attention in one table and as neutral in another. Add new statuses to the map, not to the call site.

The dot is the redundant non-colour channel. Do not remove it.


11.5 Alert

An inline message about the surface it sits on. Same six tones, 10px radius, a 2px left border in the tone's dot colour.

error and warning carry role="alert"; quieter tones do not, so a page of informational notes does not shout over a screen reader on load.

An Alert sits at the top of the content region it concerns — not floating, not fixed, not dismissible-by-timeout.


Breadcrumb → eyebrow → serif h1 → description → actions.

  • Owns the page's single h1 at 28px Lora semibold.
  • Description capped at 62ch, ink-muted.
  • Breadcrumbs: last crumb is aria-current="page" and not a link.
  • Server-safe — no use client — so only the controls passed as actions ship JavaScript.

Never render a heading next to it. Several admin pages currently hand-roll font-lora text-[32px] instead of using this; that is the bug, not the component.


11.7 EmptyState

Icon in a mist circle → title → description → action.

The title says what is not here: "No access requests yet". Not "Nothing to see here", not an emoji shrug. The description says why, or what to do about it.


11.8 StatTile and StatGrid

dt/dd pairs inside a <dl>, so a screen reader reads the label together with its value.

  • Label: 12px, uppercase, +0.06em, ink-muted.
  • Value: 26px semibold, tabular-nums, ink.
  • accent variant for the single most important figure — the sanctioned terracotta gradient wash. At most one accent tile per grid.
  • StatGrid takes 2, 3 or 4 columns and collapses to 1 on mobile.

11.9 DataTable

DataTable / Th / Td / Tr.

  • Wrapper is overflow-x-auto — the table scrolls, the page does not.
  • A visually hidden <caption> is required.
  • Th is scope="col", 11px, uppercase, +0.08em, ink-muted.
  • numeric on Th/Td right-aligns and applies tabular-nums.
  • Last row drops its bottom border.

11.10 Avatar

Initials from a name, falling back to an email, falling back to U.

terra tone fills with accent-ink so white initials clear AA. mist tone for quieter contexts — a table row, a secondary list.

aria-hidden — the person's name is always adjacent in real markup, and an avatar reading out "YJ" is noise.


11.11 Eyebrow

The ALL-CAPS section label — the one place the brand uses uppercase.

12px, medium, +0.08em, ink-muted. Optional slash prop prefixes the // device (one per surface). Always sits above a heading, never alone, never as a control.


11.12 ConfirmDialog

Destructive confirmation in a real <dialog> opened with showModal().

Why it must be a real dialog: showModal() puts the element in the browser's top layer, outside the normal flow, so no ancestor's overflow can clip it. The <details> popovers this replaced were being cut off at the card edge on the last row of a table, with no way to reach the confirm button.

Requirements:

  • m-auto on the dialog — the UA stylesheet centres a modal with margin: auto, and Tailwind's preflight resets it to 0, parking the dialog in the top-left corner.
  • Backdrop click closes: a click whose target is the dialog element itself is a backdrop click.
  • Esc closes; focus moves to the first field and returns to the trigger on close.
  • The form stays server-rendered and is passed through as children, so its server-action binding never crosses the client boundary.
  • The confirm button repeats the verb — Delete organization, not Confirm.

11.13 Component checklist

Before a new component is merged:

  • Uses only tokens — no hard-coded hex, no Tailwind default palette
  • Text colours clear 4.5:1; UI boundaries clear 3:1
  • Keyboard reachable; visible focus ring; correct tab order
  • Labelled for assistive tech (aria-label, aria-describedby, scope, caption)
  • Has designed empty, loading and error states where it can have them
  • Nothing communicated by colour alone
  • Works at 375px wide
  • Hit area ≥ 44px
  • Honours prefers-reduced-motion
  • Sentence case, verb-phrase labels
  • Server-safe unless it genuinely needs use client

This chapter as plain markdown: /docs/components.md