Quoin design

Components

The full component spec — 46 sections, on Base UI — with live examples for what's actually built in @quoin/ui.

Every component is built on @base-ui/react (1.8.0). Every component has the own skin of Quoin: calm, printed, hairline borders, small radii, and no drop shadows except on popups.

Shadow

Popups get exactly one shadow in the whole system. Popups are menus, selects, comboboxes, dialogs, tooltips, popovers, drawers and toasts. The shadow is:

/* --shadow-popup — the only elevation in the system */
box-shadow: 0 0 0 1.5px var(--color-fg), 0 12px 32px -12px rgb(30 26 21 / 0.15);

The value comes verbatim from the "Confirm · resume" dialog of Quoin in Paper (node 5DJ-0). Tooltip, PreviewCard and Menu row actions are non-modal, lighter popups. These popups can drop the 1.5px fg ring. They then keep only the offset shadow at half strength.

Built today

@quoin/ui has nineteen components now. It has twenty exports, because Field and Form ship as a pair. Every example below is live. Each example imports the real component, not a picture of the component.

Button

Live example — @quoin/ui

A Button has six variants: primary, accent, secondary, ghost, destructive and link. It has two sizes. The label is always centered (display: flex; align-items: center; justify-content: center). Quoin has no left-aligned button label.

Two of the variants are dark. The two variants are not interchangeable.

Primary is ink (--color-fg on --color-surface). It is the one dark button on a product screen, including "Create workspace" on S1. Accent is the navy --color-accent on --color-surface. It exists for exactly one place: the headline CTA of the auth flow. The CTAs are "Email me a code" on /login (A1) and "Continue" on /login/check-email (A2). Paper BFF-0 and BT8-0 draw them (07d qn01-qa6-1). On hover, accent is the same colour at 90%. When disabled, accent is at 35%. This matches the pattern of primary.

Use primary everywhere else. Two accent buttons in one product view are a bug. On all three dark fills (primary, accent, destructive), the keyboard focus ring is drawn in --color-surface. The usual accent ring would vanish against the fill (07d qn01-sync-2).

IconButton

Live example — @quoin/ui

Every icon-only action carries an aria-label. No icon-only button exists without one.

Input

Live example — @quoin/ui

Single-line text entry, built on Input of Base UI. In real screens, it is always inside a Field.Root. It has two sizes (sm 28px, md 34px). The border is 1px --color-border-strong at rest. The border is 1.5px --color-accent when the field has focus. The border is 1.5px --color-negative when the field is invalid. The border changes, never the background. Read-only and disabled fields both sit on --color-surface-2.

Input takes its own autoFocus prop for the first field of a screen (the email on /login, the name on /workspaces/new). The prop moves DOM focus itself and marks the ring visible immediately. A ref.focus() call in the page moves focus. But Chromium does not grant :focus-visible to a scripted call that has no user gesture behind it.

Field, Form

Live example — @quoin/ui

You can change this later.

A workspace URL is lower-case letters, numbers and hyphens, 3 to 40 characters.

The wrapper that every control sits inside. Field.Root carries the name that matches the error key of the server. Field.Label is --text-sm medium. Field.Description and Field.Error share one --text-sm slot 4px under the control. A field shows one or the other, never both. Form is a column with --space-4 between fields.

Avatar

Live example — @quoin/ui

Three sizes: inline (in a line of text or a chip), row (a table row, a comment) and profile (settings, the member list). The avatar falls back to initials after a 200ms delay. An image that loads fast then never flashes initials first.

Select

Live example — @quoin/ui

A closed list of options. Use it for the environment picker inside forms, for plan tiers, and for any other small, fixed choice. Do not use it for the environment switcher in the sidebar. That switcher is a distinct, always-visible control (see Design rules).

An option can also carry a one-line description under its label, in --text-xs muted. An option can be disabled. A disabled option turns both lines --color-fg-subtle. The role picker is the reference. Owner / Admin / Editor / Viewer each have their own sentence. In the invite pickers (S4 and the Invite dialog of T3), Owner is listed but disabled. The reason ("Promote someone to owner from Members & roles.") is in its description slot. An invite never carries the owner role (07d qn11a-plan-2, docs/plans/qn-11a.md Amendment 6). The visual reference is the open popup on Paper 8I2-0. OO1-0 draws the same Select closed on Editor.

Separator

Live example — @quoin/ui
FlagsExperimentsSettings

Two weights: default (--color-border) and strong (--color-border-strong). Use strong to divide a busier row from a quieter row.

Live example — @quoin/ui

The building block of the primary sidebar. It has an icon, a label, an optional count at the end, and the aria-current="page" hook of Base UI for the active state. There is no separate "active" prop to keep in sync by hand.

Live example — @quoin/ui

You've been invited to Quoin — sign in with dev@tessellate.dev to join.

upgrade-nudge-copy is frozen while its experiment runs.

Something went wrong creating your workspace. Try again — if it keeps happening, it's on us.

An inline, page-level notice. It is not Base UI. It has no timeout. A page has one Banner at most. It has three variants. Each variant is a subtle fill with the ink of the page on top. info is on --color-accent-subtle (the invite-flow entry on /login). caution is on --color-caution-subtle. negative is on --color-negative-subtle (a failed submit). A key inside the sentence takes .qn-banner-key and gets the strong colour of the variant.

Use a Banner for a notice about the page. A message about one control is Field.Error or plain caution text under that control. Never use a Banner for it. Examples are a wrong code, a taken URL, and a rate limit on the button that the person just pressed. The design rule is "caution is text only". Paper GCK-0 draws the rate-limited state that way.

OTP Field

Live example — @quoin/ui
That code's wrong. 2 attempts left before we lock it and send a new one.

The six-digit sign-in code on /login/check-email (A2), built on otp-field of Base UI. It is component 46. At desktop width, it has six 52×56 slots in --font-mono 22px/28px with an 8px gap. A paste of six digits fills all six slots. The focused slot takes the 1.5px --color-accent border. When the field is invalid, every filled slot takes 1.5px --color-negative. The sentence about attempts left appears as Field.Error.

Below desktop width, the six slots shrink evenly to fit the row inside the pane. They do not force the page wider than the viewport. The gap and the text size do not change. The visual reference is section 7 of Paper GET-0.

CopyChip

Live example — @quoin/ui

The signature "command chip". It is component 3 and the fifth of the five signature elements of Quoin. It is not a Base UI component. It is a styled Button primitive. It wraps the full command in --font-mono 12px/16px on a --color-surface-2 well. The copy glyph is at the end of the chip, in --color-fg-subtle. The whole chip is the affordance. There is no "Copy" word next to the icon. The command is never truncated. When the row is narrow, wrap the command in a ScrollArea.

One click writes the command to the clipboard. The click also holds a data-copied state for 1.6 seconds. The glyph crossfades to a --color-positive check with a one-pixel lift. Then the glyph reverts on its own. A rapid re-click restarts the hold. It does not cut the hold short. A clipboard that refuses never throws. Under reduced motion, the crossfade keeps its opacity swap and drops the lift.

The icon variant is the same control with no visible command. It is the copy button beside a shown-once key or beside the filename of a code block. It then takes its own aria-label. The visual reference is the step-1 chips of S2 on Paper 710-0 and the top-right chip on 919-0.

ScrollArea

Live example — @quoin/ui

Component 30, built on ScrollArea of Base UI (Root/Viewport/Content/Scrollbar/Thumb/Corner). It is a styled scroll surface for a long list inside a fixed-height panel. Examples are popup lists, table bodies of more than about six rows, and a code block that is wider than its column. Where Quoin uses a ScrollArea, it hides the native scrollbar chrome. The thumb is --color-border-strong at rest on a transparent 8px track. It is --color-fg-muted on hover or during a scroll. It fades in only when the pointer is over the area or the area moves. orientation is vertical (the default), horizontal or both. Never wrap a whole page in a ScrollArea. Page-level scroll stays native.

CodeBlock

Live example — @quoin/ui
src/flags.ts

A composition, not a 46th inventory section. It has these parts:

  • The --color-surface-2 well on S2 (Paper 710-0)
  • A filename in --font-mono 12px --color-fg-subtle
  • An icon CopyChip labelled Copy src/flags.ts (the filename it sits over) that copies the raw code
  • The code, in a horizontal ScrollArea. A long line then scrolls inside the block and does not widen the page.

CodeBlock takes the highlighted markup as html. The app highlights on the server with Shiki and the own token colours of Quoin. The example above passes plain <pre><code> markup. The block shows this markup when no highlighter runs.

Table

Live example — @quoin/ui
EmailRoleLast active
Mia Okaformia@tessellate.ioOwnernow
Tessellate needs at least one owner. Promote someone else first.
Sam Reyessam@tessellate.ioAdmin3 hours ago
Dev Pateldev@tessellate.ioEditor1 day ago

Component 35, built on TanStack Table. It uses the legacy, v8-compatible API behind the /legacy subpath (07d qn11a-ii-2). It has the row styling of Quoin:

  • A 34px header row on --color-surface-2 under a --color-border-strong rule
  • Body rows at --row-height-table (40px, the token that docs/03-components.md sets), separated by hairline --color-border
  • Sortable headers as plain buttons with a small chevron pair. The pair lights up on the sorted column.

Paper BIB-0 draws its member rows taller, at 52px. The token is what ships. To bring the canvas to the token is TBD: for the next design pass on T3. The column definitions set the column widths. The slot for row actions is a fixed 32px column at the end of the row. It stays in the row when it is empty. For example, the row of the signed-in person has no "⋯". The lane then lines up down the table.

rowDetail renders a second, full-width row under any row that needs one. T3 uses it for the sole-owner caution line, in --color-caution text, never negative. emptyState fills the body when there are no rows. The visual reference is the member table of T3 on Paper BIB-0. 7PB-0 (T1, environments) is the second reference, with 48px rows and the same empty slot at the end of the row.

Live example — @quoin/ui
Dev Patel's row

Component 15, built on Menu of Base UI (Root/Trigger/Popup/Item/Separator). It is the row-actions popup behind a "⋯" IconButton. The popup takes --shadow-popup like every other popup. Items are 30px high on a --color-accent-subtle highlight. A destructive item is --color-negative text on a --color-negative-subtle highlight. It is the one negative colour on the screen while the menu is open. Its label ends in an ellipsis when it opens a confirm, as "Remove member…" does.

By default, the popup opens below its trigger, end-aligned. side and align override that. The popup opens the way Select does. It scales from its trigger and fades over --transition-popup-open. It closes over --transition-popup-close. Under reduced motion, it uses opacity alone (docs/04-motion.md §3, 07d qn11a-ii-sync-1). The visual reference is Paper P05-0 (T3, the menu open on Dev Patel's row).

Dialog

Live example — @quoin/ui

Component 20, built on Dialog of Base UI (Root/Trigger/Popup/Title/Description/Close). It is the modal shell for a small form. The popup is 480px wide at most, with 20px of padding, --radius-lg corners and --shadow-popup. The title is --text-lg semibold. A Close composed onto a ghost IconButton with .qn-dialog-close-icon sits 12px inside the corner.

The panel enters over --duration-slow, scales up from 0.95 and settles up 8px. It leaves over --duration-base. The backdrop fades in with the panel and fades out faster than it faded in (docs/04-motion.md §3, 07d qn11a-ii-qa-6 and qn11a-ii-sync-1). Footer actions sit in .qn-dialog-actions, right-aligned, with the secondary Cancel before the primary. Close is otherwise a plain passthrough. A footer button composed onto it stays a footer button (07d qn11a-ii-qa-1).

The example is the Invite dialog of T3, Paper OO1-0. Work email has focus and is empty. Role defaults to Editor, with Owner shown disabled. AFS-0 (the create-key dialog of T2) is the same shell.

AlertDialog

Live example — @quoin/ui

Component 21, built on AlertDialog of Base UI. It has the shell of Dialog, but it does not close on a click outside and it has no corner close. The person answers Cancel or the destructive button. This is the one-click shape: Title, Description, Cancel and one destructive Button. The destructive Button is the only negative colour in the dialog as built. QN-11a-ii built it for "Remove member" (Paper OBX-0) and "Revoke invite" (OHZ-0). It matches the revoke-key dialog of T2, APW-0. When a mutation fails, the dialog reports the failure as caution text above the actions. The dialog does not close (07d qn11a-ii-qa-4).

The dialog enters and leaves as opacity alone, with no scale. It uses --duration-base in and --duration-fast out. This is the deliberate contrast with Dialog that docs/04-motion.md §3 asks for (07d qn11a-ii-sync-1).

The canvas also draws a small --color-negative ! before the title. The build renders no glyph, and by ruling never will (07d qn11a-ii-design-1(4), extended to every AlertDialog, not only OBX-0, by 07d qn11b-signoff-2). Each dialog already spends the one negative colour of the screen on its destructive Button. A red glyph before the title is a second use. This is against the rule of CLAUDE.md, "negative colour at most once per screen". The title stays a plain BaseAlertDialog.Title in --color-fg. The canvas keeps the glyph as a drawing convention only.

The typed-key shape is the same dialog plus one mono Field. The placeholder of the Field is the key to type back. The destructive button stays disabled until the typed value matches exactly. E8T-0 (delete workspace) and PVU-0 (delete environment) draw this shape. T1 and T7 of QN-11b built it.

Checkbox

Live example — @quoin/ui

Component 9, built on Checkbox of Base UI (Root/Indicator). It is a hairline-bordered 16px box with --radius-sm corners. The box fills --color-fg when checked. A stroked SVG path in the surface colour draws the check. keepMounted on the indicator makes the check animate in and out. The check does not pop. The label is a plain <label htmlFor> beside the box. The component ships the box alone.

Its one job in QN-11b is the "I've stored it somewhere safe" gate on the secret-shown-once Dialog of T2 (Paper AKU-0). The primary Done stays disabled until the person ticks the box, exactly as above. The two disabled boxes on the right are the states that a form leaves behind when its answer is fixed.

RadioGroup

Live example — @quoin/ui

Component 10, built on RadioGroup and Radio.Root of Base UI. @quoin/ui exports the pair as one RadioGroup object (Root/Item). Use it for a single selection among a small set of named options, when every option needs a sentence. The example is the Scope picker of T2 from the create-key Dialog (Paper AFS-0), with Server pre-selected. Each row is a <label> that wraps the Item and a two-line label/helper stack. The whole row is then the hit target.

The checked dot has the same ink-on-surface language as the check of Checkbox. Base UI moves focus with the arrow keys, and Space selects. Do not use it for on/off. That is the job of Switch. Even there, the state of a flag in a list is not a Switch (row 11).

The full spec

docs/03-components.md specifies everything else below, but it is not yet built. The table names what each component is for and where it shows up. Implementation then has a spec to build against and not a blank page.

#ComponentWhat it's for
1ButtonEvery primary action. It has six variants and two sizes. Built.
2IconButtonIcon-only actions with a required aria-label. Built.
3CopyChipA copyable CLI command, inline. It is the signature "command chip". It appears on flag pages, experiment reports and audit rows. Built.
4InputSingle-line text entry: flag keys, notes, search. Built.
5TextareaMulti-line entry, specifically the required note on every write.
6NumberFieldA percent stepper. It is the rollout % control.
7SelectA closed, short list of options. Built.
8ComboboxA searchable closed list. Use it to pick a flag key or a segment.
9CheckboxMulti-select within a form. Built.
10Radio groupSingle-select among a small set of named options. Built.
11SwitchDeliberately not used for the on/off state of a flag in a list. See the note in the spec. A list row needs a heavier confirm path than a switch implies.
12SliderThe drag surface of the rollout % control, paired with NumberField.
13Field / Label / Description / Error / Fieldset / FormThe wrapper that every form control sits inside. It sets the spacing from label to control and from control to error. Built (Field and Form; Fieldset is not).
14TabsEnvironment tabs, underline style. Use them inside a page, never to choose the environment itself.
15MenuRow actions (the "⋯" menu of a flag row). QN-11a-ii needed it first, for the single destructive item of the member row, "Remove member…". The visual reference is Paper P05-0 (T3, menu open). The popup padding is 6px and the item is 30px high. The destructive text is --color-negative with the --color-negative-subtle highlight. Built.
16ContextMenuRight-click actions on a table row.
17PopoverNon-modal detail panels, for example the quick-edit surface of a flag.
18TooltipShort clarification on hover.
19PreviewCardA hover on a flag key, anywhere, shows its current value and rule summary.
20DialogModal confirms and small forms. QN-11a-ii needed it first, for the Invite dialog of T3. The visual reference is Paper OO1-0 (and AFS-0, the create-key dialog of T2, the same 440px shell). The title is --text-lg semibold. The corner close is 12px inset. The dialog has --shadow-popup and radius 10px. Built.
21AlertDialogDestructive confirms that require the person to type the key back, for example to delete a flag. The typed field is only for the actions with the highest blast radius. A one-click confirm (Title, Description, Cancel and the destructive button) is the same component. QN-11a-ii builds that shape first. The visual reference is Paper OBX-0 (remove member) and OHZ-0 (revoke invite), matching APW-0 (revoke key of T2). The dialog is 420px. The title is plain --color-fg text with no glyph. The small ! before the title on the canvas is a drawing convention only. It is ruled out everywhere that a destructive Button already spends the one negative colour of the screen (07d qn11a-ii-design-1(4), 07d qn11b-signoff-2). The reference for the typed shape is E8T-0 (delete workspace) and PVU-0 (delete environment). It is the same dialog plus one mono Field. The placeholder of the Field is the key to type back. The destructive button stays disabled until the typed value matches exactly. Built (both the one-click shape and the typed-key shape, the latter by T1/T7 of QN-11b).
22Drawer / side sheetLarger creation flows, for example the creation of a flag.
23ToastWrite confirmations, always with an Undo action and the revision id.
24Accordion / CollapsibleThe rule stack, and the pre-registration detail block.
25AvatarWho wrote a revision, and who is assigned. Built.
26Badge / Status textColored text and a dot, deliberately not a pill, for flag status and the six verdict states.
27MeterThe rollout % bar.
28ProgressExposure collected toward the planned sample size.
29SeparatorDividing rows and sections. Built.
30ScrollAreaA styled scroll surface for long lists inside a fixed-height panel. Built.
31ToolbarTable filters above a list.
32ToggleGroupA segmented control for two to four views of one thing, for example table/chart or day/week. Never use it as the environment switcher. The environment switcher is the control of the sidebar (see Design rules).
33NavigationMenuThe header of the marketing site.
34Sidebar nav itemThe primary app sidebar's row. Built.
35TableEvery list view, built on TanStack Table with the row styling of Quoin. QN-11a-ii needed it first, for the member table of T3. The visual reference is Paper BIB-0. It has a 34px header row in --color-surface-2, 52px body rows, fixed-width columns (200 / 200 / 140 / the rest), and a fixed 32px slot at the end of the row for the "⋯" row actions. The slot is empty on the rows that have no actions (the row of the signed-in person). The lane then stays aligned. 7PB-0 (T1, environments) is the second reference. It has 48px rows, the same empty slot at the end of the row on the three seeded rows, and a plain-text count column ("2 active" / "none") where a Switch is not wanted. Built.
36Command palette⌘K. Jump to any flag, experiment or settings page.
37Empty state"Nothing rolling out right now," and similar, for every list that can be empty.
38SkeletonPlaceholders for rows and cards while data loads.
39BannerAn inline, page-level notice (a stale-write conflict, a frozen-while-running notice). Built.
40Verdict blockThe headline sentence of the report, in Source Serif. It is one of the five signature elements.
41RulerThe interval drawn as a printer's line gauge. It is one of the five signature elements.
42Locked blockThe locked state of the pre-registration. It is one of the five signature elements.
43Decision option cardA ship / rollback / inconclusive choice, shown with its projected number impact.
44AI analysis blockThe narration of the model for a report. It always cites the exact JSON field behind each number that it states.
45StampThe rolled-back / shipped decision record. It is the one place where ALL CAPS appears in the whole system.
46OTP FieldThe six-digit sign-in code on /login/check-email (A2), built on otp-field of Base UI. It has six 52×56 slots in --font-mono 22px/28px. A paste of six digits fills all six slots. A wrong code paints the border of every filled slot --color-negative 1.5px. The sentence about attempts left appears under the button. An expired code empties the slots. The countdown becomes an active "Resend code" link. The visual reference is section 7 of Paper GET-0 (C-07), cloned from the A2 artboards BT8-0 / GDW-0 / BY6-0. docs/03-components.md §46 has the written anatomy (07d qn01-plan-8). Built.

Reading this table

"Built" means that the component exists in packages/ui/src today and that a developer can import it from @quoin/ui. All other components are specified, with a shape, a set of variants and an a11y contract. They have no implementation to link to. When a team builds a component, its row here must gain a live <Example />, as the nineteen above do. The row must change in the same commit as the component.

On this page