Foundations

Tokens

Named design decisions that travel from a designer's file to a compiled binary without anyone retyping a hex code — and, in a regulated product, without anyone being able to change a safety-critical value by accident.

Stable · v1.0 shadcn/ui contract Tailwind

Overview

A token is a name bound to a value. The name expresses intent — --muted-foreground, --alarm-critical — and the value can change per theme without any component knowing. That indirection is what makes a dark theme a configuration change rather than a redesign.

In a SaMD the indirection buys something else as well: a single auditable place where safety-relevant visual decisions live. When a notified body asks how you guarantee that a high-priority alarm is red in every screen of every product, the answer is one token definition and the lint rule that protects it — not a review of four hundred components.

Intentional

A token name says what the value is for, never what it looks like. There is no --teal-button; there is --primary.

Layered

Primitives hold raw values, semantics bind them to roles, components consume only semantics. A component that reaches past its layer is a bug.

Governed

Most tokens are free to be re-themed. A small, explicitly marked set is not, because changing it changes what the interface means.

The three layers

A value travels through exactly three stops. Skipping one is what makes design systems drift.

How a value travels
1 --njt-teal-800: #0F3B35 Primitive — a raw ramp step. Has no meaning yet. Never used by a component.
2 --primary: var(--njt-teal-800) Semantic — binds the value to a role. This is the layer that changes between themes.
3 className="bg-primary" Component — consumes the role. Knows nothing about teal.
LayerPrefixExampleMay a product change it?
Primitive--njt-* --njt-teal-800No — the ramp is the brand.
Semanticshadcn names --primary, --mutedYes — this is the theming surface.
Semantic, governed--alarm-* --alarm-criticalNo — controlled characteristic.
Documentation--doc-* --doc-coral-textN/A — never ships in a product.

The shadcn contract

The semantic layer uses shadcn/ui's variable names verbatim. This is deliberate and it is the main reason to build on shadcn at all: any block, example or third-party component from the wider ecosystem drops into this system and is instantly on-brand, because it is already reaching for --primary and --muted.

/* app/globals.css */
:root {
  --background: #FDFCF8;   --foreground: #16231F;
  --card: #FFFFFF;         --card-foreground: #16231F;
  --popover: #FFFFFF;      --popover-foreground: #16231F;
  --primary: #0F3B35;      --primary-foreground: #F6F1E7;
  --secondary: #EFEDE4;    --secondary-foreground: #16504A;
  --muted: #EFEDE4;        --muted-foreground: #54635E;
  --accent: #E4EEE9;       --accent-foreground: #0F3B35;
  --destructive: #CE0E2D;  --destructive-foreground: #FFFFFF;
  --border: #DEE4DF;       --input: #DEE4DF;
  --ring: #2A8B7C;         --radius: 0.625rem;
}

.dark {
  --background: #0B2A26;   --foreground: #F6F1E7;
  --card: #0F3B35;         --primary: #3BC4AF;
  --muted: #16504A;        --muted-foreground: #A8BDB6;
  --border: #1E4A43;       --ring: #3BC4AF;
  --destructive: #D93650;
}

The alarm extension

Stock shadcn has no concept of alarm priority. It has --destructive, which means "this control destroys data" — a completely different job from "this patient may be having a STEMI." Conflating them is the single most likely way for a team new to medical software to build something unsafe.

/* Medical extension — IEC 60601-1-8 alarm priorities.
   GOVERNED: not re-themable by product, customer skin, or user preference.
   Identical in light and dark. Changing a value here requires re-validation. */
:root, .dark {
  --alarm-critical: #CE0E2D; --alarm-critical-foreground: #FFFFFF;
  --alarm-urgent:   #F2C200; --alarm-urgent-foreground:   #000000;
  --alarm-advisory: #4FB3D9; --alarm-advisory-foreground: #000000;
  --alarm-info:     #B3BEB9; --alarm-info-foreground:     #000000;
  --alarm-ok:       #1D6E62; --alarm-ok-foreground:       #FFFFFF;
}

Wiring into Tailwind

// tailwind.config.ts
theme: {
  extend: {
    colors: {
      // …stock shadcn keys…
      alarm: {
        critical: "var(--alarm-critical)",
        "critical-foreground": "var(--alarm-critical-foreground)",
        urgent:   "var(--alarm-urgent)",
        "urgent-foreground":   "var(--alarm-urgent-foreground)",
        advisory: "var(--alarm-advisory)",
        "advisory-foreground": "var(--alarm-advisory-foreground)",
        info:     "var(--alarm-info)",
        "info-foreground":     "var(--alarm-info-foreground)",
        ok:       "var(--alarm-ok)",
        "ok-foreground":       "var(--alarm-ok-foreground)",
      },
    },
  },
}

Governance

Tokens split into two governance classes. The distinction is the whole point of this page.

ClassTokensChange process
Open All shadcn semantics — --primary, --muted, --radius, spacing, motion Normal design review. Theme overrides permitted.
Governed --alarm-*, focus-ring visibility, minimum touch target, minimum body size Design change with safety consequence. Requires risk assessment, usability re-validation and a documented rationale.

Enforcement

A rule that lives only in documentation is not a control. The governed set is enforced by a dependency-free gate that runs on every pull request and fails the build on violation.

CheckAssertsCatches
parity Governed alarm tokens resolve identically in light and dark A theme quietly changing what a signal means
contrast Every declared token pair meets its documented minimum A "small tweak" pushing text below AA
sync The shipped stylesheet matches tokens/tokens.json exactly Drift between the source of truth and what actually renders
redefinition No governed token is re-declared outside its blessed block, and no governed value appears as a literal in product source A customer skin overriding an alarm hue; a hard-coded hex that survives a validated palette change
separation Brand hues stay perceptually distant (ΔE) from alarm hues A brand colour drifting close enough to alarm red to be confusable

Running it

$ npm run check:tokens

  Token governance gate — NotJustAnyMed.Tech Design System
  ──────────────────────────────────────────────────────────────
  PASS  parity        10 governed alarm tokens identical across both themes
  PASS  contrast      34 colour pairs meet their documented minimum
  PASS  sync          assets/njt-ds.css matches tokens.json
  PASS  redefinition  8 files scanned, no governed token redefined or hard-coded
  ──────────────────────────────────────────────────────────────
  note  ΔE(alarm-critical #CE0E2D, #D8593A) = 22.6
  note    ↳ below the 25 separation threshold — this is why coral is
          banned from product UI.
  ──────────────────────────────────────────────────────────────
  All governance contracts hold.

What a violation looks like

  FAIL  parity        --alarm-critical differs between themes
                      light #CE0E2D vs dark #E24359. IEC 60601-1-8 hues are
                      absolute; a theme may not change what a signal means.

  FAIL  redefinition  --alarm-urgent is declared outside the blessed blocks
                      assets/njt-ds.css:405 → #FF9900. Governed tokens have
                      exactly one definition point; a skin may not override them.

  FAIL  redefinition  Literal #CE0E2D appears in src/Widget.tsx
                      That is the value of --alarm-critical. Use the token:
                      a literal is invisible to this gate.

$ echo $?
1
The gate found real defects

On first run against this system it failed. --input had inherited shadcn's default of matching --border, giving a control boundary of 1.10:1 against the worst surface it sits on — below the 3:1 that WCAG 2.2 SC 1.4.11 requires for identifying a component. The token is now #76827C at 3.41:1 against the worst surface it can sit on, and is governed, because a clinician who cannot locate the edge of a field is a use-error risk, not a visual preference.

This is what a control is for. A design system that only documents its rules discovers this at audit; one that enforces them discovers it on the first commit.

Files

PathRole
tokens/tokens.json Single source of truth. Every token, both themes, governance class, and a rationale string on each governed entry.
scripts/colour.mjs WCAG relative luminance and contrast, CIE76 ΔE. No dependencies.
scripts/check-tokens.mjs The token gate. Exits non-zero on violation. --json for machine output.
scripts/check-docs.mjs The documentation gate — the same discipline applied to the pages that explain the tokens. Ten checks: cross-references, class and var() resolution, required sections, table scopes, unqualified validation claims, page structure, sitemap reachability, build-status counts and the reference-application register.
.github/workflows/design-system.yml Runs both on every PR as independent jobs, and retains the JSON reports for 90 days as design-control evidence.
Why the rationale field matters

Every governed token carries a written rationale in tokens.json. When a reviewer asks why high-priority alarms are #CE0E2D, the answer is in the same file as the value, in version control, with the commit that introduced it — rather than in a designer's memory.

Non-colour scales

Spacing — 4 px base

Every gap is a multiple of four. Tailwind's default scale already is, so no override is needed; the constraint is on usage, not configuration.

TokenValueTypical use
--njt-s-14pxIcon to label
--njt-s-28pxWithin a control
--njt-s-312pxBetween related controls
--njt-s-416pxCard padding, default gap
--njt-s-624pxBetween groups
--njt-s-832pxBetween sections
--njt-s-1248pxBetween major regions

Radius

One base token, two derived. Matches the brand's 6 / 10 / 16 geometry.

TokenValueUse
--radius-sm6pxButtons, inputs, chips
--radius-md8pxAlerts, popovers
--radius / -lg10pxCards, dialogs, panels

Motion

TokenValueUse
--njt-dur-180msHover, press feedback
--njt-dur-2140msColour and state change
--njt-dur-3220msEntry and exit
--njt-dur-4320msLarger surfaces, panels
--njt-ease-standardcubic-bezier(.2,0,0,1) Begins and ends at rest
--njt-ease-entercubic-bezier(0,0,0,1) Incoming elements
--njt-ease-exitcubic-bezier(.3,0,1,1) Exiting elements

Do's and don'ts

Do
<div className="bg-alarm-critical
     text-alarm-critical-foreground">

Consume the semantic role. The component stays correct when the theme changes and cannot drift from the standard.

Don't
<div className="bg-[#CE0E2D]
     text-white">

A hard-coded hex is invisible to the contrast test and the lint rule. It will survive a palette change that everything else follows.

Do
--primary: #0F3B35;   /* open  */
--alarm-urgent: #F2C200;  /* governed */

Theme the open set freely. Leave the governed set alone.

Don't
.customer-acme {
  --alarm-urgent: #FF9900; /* on-brand! */
}

A customer skin overriding an alarm hue. Now "urgent" means something different on this deployment than every other device in the room.

More do's and don'ts

Do
"alarm-urgent": {
  "class": "governed",
  "light": "#F2C200",
  "dark":  "#F2C200",
  "rationale": "IEC 60601-1-8 medium priority."
}

A governed value carries its rationale in the same file, in version control, with the commit that set it.

Don't
/* hotfix: customer said the
   yellow looked harsh */
--alarm-urgent: #FFE066;

A safety-relevant value changed in a hotfix, with the reason in a comment that will not survive the next refactor.

Do
.alert { background: var(--alarm-urgent); }

Components consume the semantic layer, so they stay correct when a theme changes and cannot drift from the standard.

Don't
.alert { background: var(--njt-warm-400); }

A component reaching past its layer into a primitive. It now has a colour with no role and no governance.

Clinical safety notes

Risk controls carried by this foundation

Trace these in your risk file (ISO 14971).

Implementation

The token layer is an installable item, and its install command used to live only in Getting started. A reader arriving here to read what the tokens are was told what they mean and not how to obtain them.

$ npx shadcn@latest add https://md.notjustany.tech/r/foundations.json

It writes the CSS variables and ships njt-tokens.json, the machine-readable contract. Changing a governed value there is a design change with a safety consequence — see the notes above.

NotJustAnyMed.Tech Design System · Tokens · v1.0 · draft for review