Design system

Design tokens

Every visual decision in ABBA UI is a CSS custom property under the --abba- prefix. Components reference tokens and never literal values, which is what makes theming a stylesheet change rather than a fork.

Two layers

Tokens come in two layers. Palette tokens name a colour — --abba-cedar-600 is a specific teal and nothing more. Semantic tokens name a role — --abba-primary is whatever colour primary actions should be. Components only ever use the semantic layer. When you rebrand, you usually only need to repoint the semantic tokens.

Palette — Cedar

The brand family: a deep teal-green. --abba-cedar-600 is the default primary and reaches 6.3:1 against white text, comfortably clearing WCAG AA.

  • --abba-cedar-50#edf6f3
  • --abba-cedar-100#d2e9e2
  • --abba-cedar-200#a6d3c7
  • --abba-cedar-300#74b8a8
  • --abba-cedar-400#45988a
  • --abba-cedar-500#2a7c6e
  • --abba-cedar-600#1f6b60
  • --abba-cedar-700#175449
  • --abba-cedar-800#123f38
  • --abba-cedar-900#0d2c27
  • --abba-cedar-950#071a17

Palette — Ember

The accent family: a warm burnt orange. --abba-ember-500 is deliberately darker than a true mid-tone so it reaches 5.0:1 with white text instead of stalling at 4.4:1 — a mid-tone orange that looks right almost never passes.

  • --abba-ember-50#fdf3ea
  • --abba-ember-100#f9e0c9
  • --abba-ember-200#f0c094
  • --abba-ember-300#e39d5f
  • --abba-ember-400#d27e33
  • --abba-ember-500#a85714
  • --abba-ember-600#984e13
  • --abba-ember-700#7a3d10
  • --abba-ember-800#5c2e0d
  • --abba-ember-900#3f200a

Palette — Neutrals

Warm-tinted rather than pure grey. The warm cast is a deliberate identity choice: it is the single most effective defence against a system reading as generic template output.

  • --abba-neutral-0#ffffff
  • --abba-neutral-25#fcfbf9
  • --abba-neutral-50#faf9f7
  • --abba-neutral-100#f4f2ee
  • --abba-neutral-200#e8e4dd
  • --abba-neutral-300#d6d0c6
  • --abba-neutral-400#b0a89c
  • --abba-neutral-500#857c71
  • --abba-neutral-600#635b52
  • --abba-neutral-700#47413a
  • --abba-neutral-800#2e2925
  • --abba-neutral-900#1c1815
  • --abba-neutral-950#12100e

Semantic roles

These are the tokens components actually consume, and the ones to override when rebranding.

  • --abba-primaryvar(--abba-cedar-600)
  • --abba-primary-hovervar(--abba-cedar-700)
  • --abba-primary-activevar(--abba-cedar-800)
  • --abba-primary-foregroundvar(--abba-neutral-0)
  • --abba-primary-subtlevar(--abba-cedar-50)
  • --abba-primary-subtle-foregroundvar(--abba-cedar-800)
  • --abba-accentvar(--abba-ember-500)
  • --abba-accent-hovervar(--abba-ember-600)
  • --abba-accent-activevar(--abba-ember-700)
  • --abba-accent-foregroundvar(--abba-neutral-0)
  • --abba-accent-subtlevar(--abba-ember-50)
  • --abba-accent-subtle-foregroundvar(--abba-ember-800)

State colours

Each state has a solid pairing and a subtle pairing. --abba-warning-foreground is dark rather than white: white on amber cannot reach AA without pushing the hue muddy, so the system takes the dark foreground instead of a worse colour.

  • --abba-success#1f7a4d
  • --abba-success-foregroundvar(--abba-neutral-0)
  • --abba-success-subtle#e8f5ee
  • --abba-success-subtle-foreground#10502f
  • --abba-success-border#a8d8be
  • --abba-warning#b7791f
  • --abba-warning-foregroundvar(--abba-neutral-900)
  • --abba-warning-subtle#fcf3e3
  • --abba-warning-subtle-foreground#7a4e10
  • --abba-warning-border#efd49b
  • --abba-danger#b42318
  • --abba-danger-hover#94170e
  • --abba-danger-active#7a1109
  • --abba-danger-foregroundvar(--abba-neutral-0)
  • --abba-danger-subtle#fdecea
  • --abba-danger-subtle-foreground#7a1109
  • --abba-danger-border#f3b4ae
  • --abba-info#2c6ba8
  • --abba-info-foregroundvar(--abba-neutral-0)
  • --abba-info-subtle#e9f1f9
  • --abba-info-subtle-foreground#1c4a75
  • --abba-info-border#aecbe6

Surfaces and borders

  • --abba-backgroundvar(--abba-neutral-0)
  • --abba-background-subtlevar(--abba-neutral-50)
  • --abba-background-raisedvar(--abba-neutral-0)
  • --abba-foregroundvar(--abba-neutral-900)
  • --abba-mutedvar(--abba-neutral-100)
  • --abba-muted-foregroundvar(--abba-neutral-600)
  • --abba-overlayrgb(18 16 14 / 55%)
  • --abba-bordervar(--abba-neutral-300)
  • --abba-border-subtlevar(--abba-neutral-200)
  • --abba-border-strongvar(--abba-neutral-400)

Spacing

A 4px base scale. Layout components take these as numbers — gap={4} resolves to var(--abba-space-4) — so spacing stays on the scale without you writing the variable out.

  • --abba-space-00
  • --abba-space-10.25rem
  • --abba-space-20.5rem
  • --abba-space-30.75rem
  • --abba-space-41rem
  • --abba-space-51.25rem
  • --abba-space-61.5rem
  • --abba-space-82rem
  • --abba-space-102.5rem
  • --abba-space-123rem
  • --abba-space-164rem
  • --abba-space-205rem
  • --abba-space-246rem

Radii

  • --abba-radius-none0
  • --abba-radius-sm6px
  • --abba-radius-md10px
  • --abba-radius-lg14px
  • --abba-radius-xl20px
  • --abba-radius-full9999px

Typography

The library downloads no webfont. --abba-font-sans resolves to the platform UI stack; override it with your own loaded family and everything follows.

Type scale
TokenValueUsed for
--abba-font-sansui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif
--abba-font-monoui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace
--abba-font-size-xs0.75remCaptions, shortcut hints
--abba-font-size-sm0.875remSecondary text, dense UI
--abba-font-size-md1remBody copy — the default
--abba-font-size-lg1.125remLead paragraphs
--abba-font-size-xl1.375remSection headings
--abba-font-size-2xl1.75remPage headings
--abba-font-size-3xl2.25remPage titles
--abba-font-size-4xl3remDisplay
--abba-font-weight-regular400
--abba-font-weight-medium500
--abba-font-weight-semibold600
--abba-font-weight-bold700
--abba-line-height-tight1.2
--abba-line-height-snug1.35
--abba-line-height-normal1.55
--abba-line-height-relaxed1.75
--abba-letter-spacing-tight-0.018em
--abba-letter-spacing-normal0
--abba-letter-spacing-wide0.04em

Weights are --abba-font-weight-regular (400), medium (500), semibold (600) and bold (700). Line heights run tight (1.2), snug (1.35), normal (1.55) and relaxed (1.75).

Elevation

Five shadow steps, tinted warm rather than neutral black — a pure black shadow over a warm neutral surface reads as grey dirt. The dark theme replaces them with higher-opacity black, because a tinted shadow is invisible on a near-black background.

  • --abba-shadow-xs — hairline lift, used on inputs
  • --abba-shadow-sm — resting cards
  • --abba-shadow-md — hovered cards, dropdown menus
  • --abba-shadow-lg — popovers
  • --abba-shadow-xl — dialogs

Motion

Duration and easing tokens
TokenValueUsed for
--abba-duration-instant80msState flips with no perceived travel
--abba-duration-fast140msHover, focus, small colour changes
--abba-duration-normal220msMenus, popovers
--abba-duration-slow320msDialogs and larger surfaces
--abba-ease-standardcubic-bezier(0.2, 0, 0, 1)Default curve
--abba-ease-outcubic-bezier(0, 0, 0.2, 1)Entering the screen
--abba-ease-incubic-bezier(0.4, 0, 1, 1)Leaving the screen

Every animated component honours prefers-reduced-motion. Where motion carries meaning — the spinner — it degrades to a static state rather than disappearing, because the user still needs to know something is happening.

Z-index

A named scale, so overlay ordering is decided once here instead of being re-litigated with z-index: 9999 in every application.

Stacking order
TokenValue
--abba-z-base0
--abba-z-raised10
--abba-z-sticky100
--abba-z-overlay1000
--abba-z-modal1100
--abba-z-popover1200
--abba-z-toast1300
--abba-z-tooltip1400

Focus and disabled

  • --abba-focus-ring, --abba-focus-ring-width (3px) and --abba-focus-ring-offset (2px) — one ring treatment across every focusable component.
  • --abba-disabled-opacity (0.55) — applied consistently so disabled controls read the same everywhere.

Using tokens in your own CSS

/* Tokens are ordinary CSS custom properties, so your own
   styles can consume them directly. */
.myPanel {
  padding: var(--abba-space-6);
  border: 1px solid var(--abba-border-subtle);
  border-radius: var(--abba-radius-lg);
  background-color: var(--abba-background-raised);
  box-shadow: var(--abba-shadow-sm);
  color: var(--abba-foreground);
}