Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The design system

What every built-in widget is drawn to, as it is built: Nord's sixteen colours behind alias tokens, one type scale, a 4 px ramp, three motion durations, and the rules that keep them honest.

By the end of this chapter you know which --gb-* tokens exist and what each is for, what the type classes measure, how big a control is at each density, and which parts of the written design are built and which are not. The full specification is docs/design-system.md in the repository. This page is the built subset.

Principles

  1. Desktop only. Pointer and keyboard first. Hit targets are 32 by 32 at regular density.
  2. Opaque first. Every surface is designed opaque. Frost is an enhancement layer, and it is not built.
  3. Token or extend. A screen that needs a value no token names extends the system rather than improvising.
  4. Keyboard complete. Every interaction is reachable without a pointer and :focus-visible is always legible.
  5. Deterministic. Embedded fonts, CPU raster and token-driven colour: the same markup renders the same bytes on every machine, and golden images are the arbiter.

Colour

The raw palette is Nord, exposed as --nord0 to --nord15 in both themes. Widgets never read the palette. They read alias tokens, and only the alias tokens change between the two themes.

TokenHexTokenHex
--nord0#2E3440--nord8#88C0D0
--nord1#3B4252--nord9#81A1C1
--nord2#434C5E--nord10#5E81AC
--nord3#4C566A--nord11#BF616A
--nord4#D8DEE9--nord12#D08770
--nord5#E5E9F0--nord13#EBCB8B
--nord6#ECEFF4--nord14#A3BE8C
--nord7#8FBCBB--nord15#B48EAD

The first four are Polar Night, the next three Snow Storm, then Frost, then Aurora. Aurora hues carry meaning: danger, warning, success, info and chart series. They are never decoration on a control.

Alias tokens

panel { background: var(--gb-surface); color: var(--gb-text); border: 1px solid var(--gb-border) }
TokenDarkLightFor
--gb-bgnord0nord6the window
--gb-surfacenord1#ffffffa panel, a card, a menu
--gb-surface-2nord2nord5a second plate that is distinct, in no direction
--gb-surface-raisednord2#ffffffone step up
--gb-surface-sunkenblack at 22%black at 7%a field’s well
--gb-textnord6nord0text
--gb-text-mutednord4nord3secondary text
--gb-text-placeholdernord6 at 60%nord0 at 75%a field’s placeholder
--gb-bordernord3nord4a divider, a card edge
--gb-border-strongwhite at 20%black at 16%an edge that must read
--gb-accentnord8#5c7ea8the primary action, a selected tab
--gb-focusnord8nord10the focus ring
--gb-selectionnord10 at 40%nord8 at 30%selected text and rows
--gb-scrimblack at 55%black at 40%the veil behind a dialog or a tour

A semantic hue has three ranks. --gb-danger is what danger is, a fill; --gb-danger-fill is what you may put words on; --gb-danger-line is what you may draw a glyph or a border with on a surface, and it clears 3:1 against it. --gb-warning, --gb-success and --gb-info have -line ranks for the same reason, and -text ranks for words on their fills (ADR-0175).

Above the aliases sit component tokens: --gb-button-bg, --gb-button-bg-hover, --gb-checkbox-mark-checked, --gb-toggle-track-bg, --gb-slider-thumb-border, --gb-chart-1 to --gb-chart-8, and their kin. An application may override component tokens, never structure. Every one is listed in nord-dark.css with a comment saying why it exists.

Every text and surface pair in both themes is measured at 4.5:1, and every mark against the box it is drawn in at 3:1, by ContrastTest in CI.

Spacing

The base unit is 4 px. The legal ramp is 2, 4, 8, 12, 16, 20, 24, 32, 40, 48 and 64, and nothing off it. Component padding is 8 or 12, the gap between related controls 8, between groups 16, and the window’s content margin 16. The ramp is a rule, not a set of tokens: a stylesheet writes gap: 8px.

Type

column {
  text class="display" "Goldberry"
  text class="title" "Preferences"
  text class="heading" "Appearance"
  text class="body" "Default UI text."
  text class="body-strong" "Emphasis, and a button's label."
  text class="caption" "Secondary metadata"
  text class="mono" "goldberry.log.level=TRACE"
}
ClassFaceSize / lineTokens
displayInter 60028 / 34--gb-font-display, --gb-line-display
titleInter 60020 / 26--gb-font-title, --gb-line-title
headingInter 60015 / 20--gb-font-heading, --gb-line-heading
bodyInter 40013 / 18--gb-font-body, --gb-line-body
body-strongInter 60013 / 18the body tokens and --gb-weight-strong
captionInter 40011 / 14--gb-font-caption, --gb-line-caption
monoJetBrains Mono 40013 / 18--gb-font-code, --gb-line-code

The two weights are --gb-weight-regular at 400 and --gb-weight-strong at 600, and each is a face rather than an axis. Inter ships upright and italic in both weights. The numbers live in the theme, so a large-text theme moves all of them at once. Text scale from 90% to 150% is a renderer switch beside them, and scales the text without the boxes (Text, fonts and icons).

Shape, elevation and materials

Radii are literals in controls.css: 4 on fields, text areas and the checkbox glyph; 8 on buttons, cards, panels, toasts and messages; 12 on dialogs; a full radius on badges, chips, progress bars, thumbs and step markers.

Elevation is three shadow tokens, and a rule never writes the numbers:

card   { box-shadow: var(--gb-elevation-1) }
dialog { box-shadow: var(--gb-elevation-2) }
TokenGeometryDark alphaLight alpha
--gb-elevation-10 2px 8px44%16%
--gb-elevation-20 8px 32px56%22%
--gb-elevation-30 16px 48px68%28%

The geometry is the same in both themes and the alpha is not, which is why they are tokens. A card wears level 1 and lifts to 2 on card.interactive:hover; a dialog, a toast and a tour card wear level 2; a popup is an edge rather than a shadow, because it is drawn in a window sized to itself and a shadow would be clipped (ADR-0312). Level 3 is defined and used by nothing.

Materials. Of the three the specification names, opaque is every surface, and veil is built as --gb-scrim behind a dialog and a tour. frost is not built: there is no backdrop-filter in the CSS subset and no blur in the rasterizer. A design that wants a frosted panel gets an opaque raised one, which is the fallback the specification mandates anyway.

Icons

Lucide, 1544 icons on a 24 by 24 grid with a 2 px round-capped stroke, compiled into goldberry-core as path data. Display sizes are 16, 20 and 24, and an icon in a button’s lead slot is 16. An icon is tinted by color, like text. An icon-only control needs an accessible name (Text, fonts and icons).

Motion

button        { transition: background-color var(--gb-motion-fast) ease-enter }
button:active { transition: background-color 0ms }
TokenValueFor
--gb-motion-fast100 msstate feedback: hover, check, selection
--gb-motion-base160 mscomponent transitions: popover, tabs, toggle
--gb-motion-overlay240 msoverlays: dialog, toast

Three easing keywords: ease-enter decelerates, ease-exit accelerates, linear is for continuous indicators. The rules are: input feedback is instant and only its release fades; exits are faster than enters; the focus ring is never delayed; nothing loops except a spinner, an indeterminate progress and a skeleton; and under reduced motion every transition is zero and keyframe animations do not run. The mechanics are in Styling.

States

Every control renders rest, :hover, :active, :focus-visible and :disabled, and where it applies :checked, :indeterminate and :invalid. Hover moves the surface one step and never the text colour. Disabled is 45% opacity on the whole control and never a colour remap, so a disabled danger button still reads as danger.

Focus

button:focus-visible,
checkbox:focus-visible { outline: 2px solid var(--gb-focus); outline-offset: 2px }

One focus owner per window. The ring is 2 px of --gb-focus, 2 px outside the control, following its radius, and it appears only for keyboard focus. It is one rule over a list of types in the base layer, not one rule per control. A composite is one Tab stop with the arrow keys moving inside it (ADR-0073).

Keyboard conventions

host.shortcut(Shortcut.primary(Key.S), this::save);     // Cmd+S on macOS, Ctrl+S elsewhere
host.shortcut("Primary+Shift+Z", this::redo);

Accelerators are written against the desktop’s primary modifier, which has a name: Primary in a string, Shortcut.primary(key) in Java. It is Cmd on macOS and Ctrl everywhere else, and nothing is translated: a shortcut that says Ctrl means the control key on every desktop (ADR-0378).

In a dialog Enter presses the affirmative action and Escape the dismissive one, and the affirmative sits on the right. A button activates on Space and Enter; a checkbox on Space only, so Enter still reaches the form’s default action. Tab order is document order.

Density

RegularCompact
control height, --gb-control-height3228
list row, --gb-list-row-height3226
table header, --gb-table-header-height3630
code-input box40 by 4836 by 44
calendar day3228

Density.COMPACT is those tokens on :root and nothing else. The 16 px glyph inside a checkbox or a radio does not shrink, so compact costs margin around the target rather than a smaller target (ADR-0074).

Scrollbars

Overlay by default: a 6 px thumb over the content that widens to 10 px with a track on hover, with no gutter reserved. Scrollbars.ALWAYS swaps in a classic 12 px gutter with an 8 px thumb and a --gb-surface-2 track, and a layout must survive the gutter appearing (ADR-0364).

Component metrics

Heights at regular density, with compact in brackets, as controls.css sets them.

ComponentMetrics
buttonheight 32 (28); padding-x 12; icon gap 6; radius 8; body size at the strong weight
text-input, selectheight 32 (28); padding-x 8; radius 4; fill --gb-surface-sunken
text-areapadding 6/8; radius 4
checkbox, radioglyph 16; hit 32; label gap 8
toggletrack 36 by 20; thumb 16
slidertrack 4; thumb 16 with a 1 px edge
knob32 or 48 across
menu rowheight 32 (28); padding-x 8
tabheight 32 (28); padding-x 12
list rowheight 32 (26); padding-x 12
table headerheight 36 (30)
badgeheight 20; min-width 20; padding-x 4; full radius
chipheight 24; padding-x 8; radius 12
progresstrack 4; full radius
cardpadding 12; gap 8; elevation 1
panelradius 8
dialogpadding 24; min-width 320; max-width 80%; radius 12; elevation 2
toastwidth 360; padding 12/16; radius 8
tooltippadding 8/12; radius 4; max-width 320
messagepadding 12/16; radius 8; icon gap 12
steps marker24, full radius
code-input box40 by 48 (36 by 44); radius 4

Accessibility baseline

Built: contrast measured in CI for both themes; every control operable from the keyboard, with the per-widget maps in each chapter; text scale to 150% without the boxes moving; reduced motion; hit targets of 32 at regular density; and a Role and an accessible name on every focusable widget, enforced by a sweep over the source tree.

Not built: a screen-reader bridge. Nothing on any platform reads the semantics tree, and the bridge is on hold with no milestone owning it (ADR-0440). The tree stays, so a bridge would be an adapter rather than a rearchitecture.

Governance

A new widget enters the canon with a specification, a metrics row and gallery coverage in both themes before code, and the alias tokens, the component contracts and the metrics tables are the stable tier.

Read more