We now build a lot of RN's tools with AI coding agents. Two things keep every screen consistent, accessible, and unmistakably Red Nucleus: a design system written as a document (design.md) and a front-end build agent that is bound to it. This is a short tour of both, and of where your work sits in the middle of it.
A quick note from me. Rachel, this is an early draft I put together to show how we're now building Red Nucleus tools with AI and, more to the point, where design leads it. Nothing here is set in stone and your view genuinely shapes where it goes next, so please be as blunt as you like. There's a quick feedback box at the very bottom. No rush, and thanks for taking a look. — Ben
If you read nothing else.
An AI agent can write a working screen in minutes, but left unguided it invents its own spacing, its own colours, and “consumer-flashy” effects that read as untrustworthy to a pharma audience. So we don't let it design. We give it a design system it must build from, and a designer (you) who shapes that system.
design.mdThe RN design system, written as a document the agents read as instructions: brand colours, type, spacing, components, motion, and accessibility. One source of truth.
The AI that actually builds the UI. It is required to build only from design.md's tokens and components, and its work is checked by an automated accessibility gate before it ships.
Result: every RN tool looks like one calm, professional system and clears accessibility by construction, not by luck. And because the system is a document, your design decisions become the rules the AI follows.
design.md isA design system you can read — the same idea as a Figma library, expressed so an AI can build from it exactly.
Think of it as the written form of a design system. Instead of a component library in Figma, it is a structured document that defines the same decisions in words and values an agent can follow precisely. It has seven parts:
The clever bit is that colours (and everything else) are defined in three layers, so a single change ripples everywhere and dark mode is free:
crimson-700 = #A6192EA designer changes “primary = crimson-700” once, and every button, link and active tab across every RN tool updates. The AI can never reach past this and hard-code a stray colour — that's a review reject.
design.mdThis is the actual document the agents build from, in full — exactly as it governs every RN screen. Have a scroll; you don't need to read every line, but it's all here and it's all yours to shape.
# Red Nucleus AI Development Front-End Design Standard
> **Status:** Living standard. This is the repository `design.md` and is injected as context into AI coding agents (Claude Code / Codex) and their sub-agents alongside `security.md`. Owner: AI engineering leads + design. Review cadence: quarterly, and on any change to the approved stack (Tailwind / shadcn / Radix) or the RN brand.
>
> **How to read this:** This governs how agents build **business web apps** at RN — internal tools and client-facing products. The audience skews **non-technical** (BD / commercial / medical staff). The house aesthetic is **calm, legible, accessible — not flashy**. Every rule is concrete and buildable. Where a rule is a hard requirement it says **MUST**; where it is a default that may be overridden with reason it says **SHOULD**.
>
> **Stack (fixed):** Tailwind CSS + **shadcn/ui** (Radix UI primitives + React Aria for anything Radix doesn't cover) for components; **Tremor / Recharts** for charts; **TanStack Table** for data grids; **Magic UI / Framer Motion** for restrained polish only. Do not introduce a second component library or a CSS-in-JS runtime without sign-off.
>
> **Provenance:** Synthesised from mature public design systems (IBM Carbon, Shopify Polaris, GitHub Primer, Radix Colors, shadcn/ui) and WCAG 2.2. The **colour palette (§2.3) is RN's real brand**, extracted from the live site, the logo, and existing branded work, then extended into accessible ramps and **validated with WCAG contrast maths** — it is not a generic placeholder. The **accessibility standard, regulatory scope, and enforceable definition-of-done live in §7**; target is **WCAG 2.2 AA** (`REC-WCAG22-20241212`).
---
## 1. Purpose, Scope & Core Principles
### 1.1 Why this exists
RN's tools are built largely by AI coding agents. Left unguided, agents produce inconsistent spacing, ad-hoc colours, inaccessible controls, and "consumer-flashy" motion that reads as untrustworthy to a pharma-commercial audience. This standard gives agents a **single token vocabulary and component contract** so every RN app looks like one calm, professional system and clears WCAG 2.2 AA by construction.
### 1.2 Scope
**In scope:** all RN-built web front-ends — the BD prospecting dashboard, internal admin tools, and client-facing products. Covers design tokens, typography, component specs, motion, and mandatory UI states.
**Out of scope:** marketing/brand microsites (governed by brand), native mobile, and back-end/API design (governed by `security.md`).
### 1.3 Core principles
1. **Calm over clever.** Business users are here to complete a task, not to be impressed. Prefer whitespace, hierarchy, and restraint over gradients, glows, and animation. If an effect draws attention to itself, remove it.
2. **Tokens, never magic numbers.** No raw hex, no arbitrary `px` in components. Every colour, size, space, radius, shadow, and duration comes from a token (§2). Arbitrary Tailwind values (`w-[437px]`) are a code-review smell.
3. **Accessible by default, not as a retrofit.** WCAG 2.2 AA is the floor (§ throughout). Focus-visible, keyboard operability, contrast, and target size are non-negotiable — the same "untrusted by default" discipline `security.md` applies to code.
4. **Radix/shadcn for behaviour; tokens for looks.** Never hand-roll a dropdown, dialog, or combobox — use the accessible primitive and skin it with tokens. Behaviour bugs (focus trap, ARIA, keyboard) are the expensive kind.
5. **Every data view handles four states.** loading, empty, error, no-permission (§6). A view that only handles the happy path is unfinished.
6. **Non-technical clarity.** Plain language in labels, empty states, and errors. Define jargon inline. No dead-end error like "Error 500" — say what happened and what to do.
7. **Density with air.** Business UI is data-dense, but dense ≠ cramped. Use the 4px spacing scale deliberately; a 14px body with 1.5 line-height and generous row padding beats a shrunk-to-fit table.
---
## 2. Design-Token Architecture
### 2.1 The three-tier model
Tokens flow in one direction: **primitive → semantic → component**. Components reference *only* semantic tokens (or component tokens); semantic tokens reference *only* primitives; primitives reference nothing. This is how Carbon, Polaris, and Primer all structure their systems, and how shadcn's CSS-variable theming works in practice.
| Tier | What it is | Names a… | May reference | Example |
|---|---|---|---|---|
| **1. Primitive** (a.k.a. reference / global) | The raw palette — every colour ramp, every step of every scale. Context-free. | *value* | nothing | `--rn-blue-600`, `--rn-gray-100`, `--rn-space-4`, `--rn-radius-md` |
| **2. Semantic** (a.k.a. system / alias) | Role-based meaning. This is where light/dark theming lives and where a value is chosen for a *purpose*. | *intent* | primitives | `--color-primary`, `--color-bg-surface`, `--color-text-muted`, `--color-border`, `--color-danger` |
| **3. Component** | Per-component overrides for the rare case a component needs its own knob. Most components skip this tier and use semantic tokens directly. | *part* | semantic (never primitive) | `--button-primary-bg`, `--input-border`, `--card-bg` |
**Rule:** a component that reaches past its tier (e.g. a button styled with `--rn-blue-600` directly) breaks theming and is a review reject. Skin with `--color-primary`, which resolves to the right primitive in each theme.
### 2.2 Naming convention
```
Primitive: --rn-{category}-{ramp}-{step} e.g. --rn-color-blue-600, --rn-space-4, --rn-radius-lg
Semantic: --color-{role}[-{modifier}] e.g. --color-bg-surface, --color-text-muted, --color-border-strong
Component: --{component}-{part}[-{variant}][-{state}] e.g. --button-primary-bg-hover
```
- `--rn-` namespaces primitives so they never collide with Tailwind's or a client's tokens.
- Semantic colour roles group as `bg-*` (surfaces), `text-*` (foregrounds), `border-*`, `interactive-*` (primary/secondary/etc), and `status-*` (success/warning/danger/info). shadcn's own names (`--background`, `--foreground`, `--primary`, `--muted`, `--ring`) are a subset of this and MUST be kept working so shadcn components render — map them as aliases.
### 2.3 Colour — the RN palette (brand-derived, contrast-validated)
**Strategy.** The values below are RN's **real brand colours** (from the live site, the logo, and the branded security site), extended into accessible 50→900 ramps. Define primitive ramps once; define semantic roles that reference them; theme by re-pointing the semantic layer in `:root` (light) and `.dark` (dark) — components never change. Author the ramps in OKLCH or HSL; the **hex values here are the source of truth**. Every interactive/text pairing was validated with WCAG maths (evidence table below).
**The RN decision on red (RESOLVED — encoded here; do not re-litigate per screen).** RN's brand is red, and red also conventionally signals danger. Resolution:
- **Brand crimson `#A6192E`** is the interactive **primary** (buttons, links, active nav). It is genuinely accessible — **7.5:1 on white (AAA)** as text *and* white-on-crimson is **7.5:1** as a fill — unlike the vivid logo red `#ED1C24` (only **4.4:1**), which is reserved for the **logo / brand mark only**.
- **Danger** uses a **distinct scarlet `#C0392B`** — more orange than the brand crimson, visibly different.
- WCAG 1.4.1 forbids colour-as-the-only-signal regardless, so **every destructive action carries an icon + an explicit verb** ("Delete") and, when irreversible, a confirm step — the crimson/scarlet proximity never has to carry meaning alone.
**Primitive ramps** (`--rn-color-{ramp}-{step}`, 50→900):
| Ramp | 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | Role |
|---|---|---|---|---|---|---|---|---|---|---|---|
| **crimson** | `#FCF0F2` | `#F8E0E3` | `#F5BDC5` | `#EE8C9A` | `#E65B70` | `#E0314B` | `#CA1E38` | **`#A6192E`** | `#811324` | `#5B0E19` | brand + primary |
| **slate** | `#F5F6F7` | `#EAEBED` | `#D3D7DE` | `#B4BAC6` | `#949DAE` | `#788499` | `#647084` | `#525C6D` | `#404754` | `#0F1B2D`* | text, borders, surfaces |
| **gold** | `#FBF8F2` | `#F6F1E2` | `#F0E4C2` | `#E4D095` | `#D9BC68` | `#D0AA41` | `#BA952E` | `#997B26` | `#765F1E` | `#544315` | secondary accent |
| **green** | `#F2FBF4` | `#E2F6E8` | `#C2F0D0` | `#95E4AD` | `#69D98A` | `#42CF6C` | `#2FB958` | `#1F7A3A` | `#1E7638` | `#155328` | success |
| **amber** | `#FEF8EE` | `#FCF1DC` | `#FFE5B2` | `#FFD27A` | `#FFBF42` | `#FFAE12` | `#E89900` | `#BF7E00` | `#946200` | `#694500` | warning |
| **scarlet** | `#FDF3F1` | `#FBE4E0` | `#F6C6BE` | `#EE9E90` | `#E37563` | `#D6503B` | `#C0392B` | `#9E2C20` | `#7B221A` | `#571712` | danger / destructive |
| **blue** | `#F3F6F9` | `#E5ECF3` | `#C8DAE9` | `#A0BED9` | `#78A3CA` | `#558BBC` | `#4277A6` | `#335C81` | `#2A4C6A` | `#1E354B` | info, neutral emphasis |
\* `slate-900` is RN's deep ink `#0F1B2D` (**17:1** on white) — the darkest text/surface. Mid-slate steps carry borders and muted text.
**Semantic colour roles (the contract components use):**
| Semantic token | Light source | Dark source | Use |
|---|---|---|---|
| `--color-bg-canvas` | `slate-50` | `slate-900` | App background behind everything |
| `--color-bg-surface` | `white` | `slate-800` | Cards, panels, table surface |
| `--color-bg-subtle` | `slate-100` | `slate-800` | Zebra rows, hovered rows, inset wells |
| `--color-bg-muted` | `slate-100` | `slate-700` | Disabled fills, skeleton base |
| `--color-text-primary` | `slate-900` (`#0F1B2D`) | `slate-50` | Default body + headings |
| `--color-text-secondary` | `slate-700` | `slate-200` | Supporting text |
| `--color-text-muted` | `slate-600` (`#647084`) | `slate-400` | Metadata, placeholder, captions |
| `--color-text-on-primary` | `white` | `white` | Text on the crimson fill |
| `--color-border` | `slate-200` | `slate-700` | Default hairline borders |
| `--color-border-strong` | `slate-300` | `slate-600` | Inputs, dividers needing weight |
| `--color-interactive-primary` | `crimson-700` (`#A6192E`) | `crimson-500` | Primary buttons, links, active nav |
| `--color-interactive-primary-hover` | `crimson-800` | `crimson-400` | Hover of the above |
| `--color-ring` | `crimson-600` | `crimson-400` | Focus ring (2px, 2px surface offset; ≥ 3:1 vs adjacent) |
| `--color-accent` | `gold-700` (`#997B26`) | `gold-500` | Sparing highlight (never for body text — see note) |
| `--color-status-success` | `green-700` (`#1F7A3A`) | `green-500` | Positive state / valid |
| `--color-status-warning` | `amber-800` (`#946200`) | `amber-400` | Caution (text; amber-500 for fills) |
| `--color-status-danger` | `scarlet-600` (`#C0392B`) | `scarlet-500` | Error, destructive |
| `--color-status-info` | `blue-700` (`#335C81`) | `blue-500` | Neutral information |
Each status role SHOULD also have a `-subtle` background variant (e.g. `--color-status-danger-subtle` = `scarlet-50` / `scarlet-900`) for alert/badge fills, so status is never conveyed by a single saturated colour alone.
**Contrast (MUST):** body/label text ≥ **4.5:1** vs its background; large text (≥ 18.66px bold or 24px) and non-text UI (borders of active controls, meaningful icons, focus ring) ≥ **3:1** (WCAG 2.2 AA — 1.4.3 / 1.4.11). Verify both themes. **Never** convey state by colour alone (1.4.1) — pair with icon, label, or shape.
**Validated pairings (evidence):**
| Pairing | Ratio | Grade |
|---|---|---|
| crimson-700 `#A6192E` text on white | 7.50:1 | AAA |
| white on crimson-700 (primary button) | 7.50:1 | AAA |
| ink `#0F1B2D` body text on white / cream | 17:1 / 16:1 | AAA |
| info blue-700 `#335C81` on white | 7.03:1 | AAA |
| gold-800 `#765F1E` text on white | 6.13:1 | AA |
| muted slate-600 `#647084` on white | 5.9:1 | AA |
| success green-700 `#1F7A3A` on white | 5.4:1 | AA |
| scarlet-600 `#C0392B` on white | 4.5:1 | AA |
| **gold-600 `#B8942E` as text on white** | 2.87:1 | ✗ **fill/accent only — use gold-800 for text** |
| **amber-600 as text on white** | 3.6:1 | ✗ **large/fill only — use amber-800 for text** |
| **logo red `#ED1C24` on white** | 4.38:1 | ⚠ **large/UI only — mark use only** |
Takeaway baked into the tokens: **gold and amber are fills/accents, not text colours** — their `-800` steps are the text-safe versions; the semantic roles above already point at those.
### 2.4 Typography tokens
See §3 for the full type scale and font strategy. Tokenise as `--font-sans`, `--font-mono`, plus a `--text-{role}` set each carrying size + line-height + weight.
### 2.5 Spacing scale (4px base)
One base unit = **4px** (`0.25rem`). This matches Tailwind's default spacing scale exactly, so tokens and Tailwind utilities stay 1:1. Use the scale for *all* padding, margin, and gap.
| Token | rem | px | Typical use |
|---|---|---|---|
| `space-0` | 0 | 0 | reset |
| `space-1` | 0.25rem | 4 | icon-to-text gap, tight inline |
| `space-2` | 0.5rem | 8 | compact padding, chip padding |
| `space-3` | 0.75rem | 12 | input padding-y, small gaps |
| `space-4` | 1rem | 16 | default component padding, form field gap |
| `space-5` | 1.25rem | 20 | — |
| `space-6` | 1.5rem | 24 | card padding, section inner gap |
| `space-8` | 2rem | 32 | between form sections |
| `space-10` | 2.5rem | 40 | — |
| `space-12` | 3rem | 48 | page section spacing |
| `space-16` | 4rem | 64 | major layout gaps |
| `space-24` | 6rem | 96 | page top/bottom rhythm |
Half-steps `space-0.5` (2px) and `space-1.5` (6px) exist for hairline nudges only. **MUST NOT** use off-scale values (e.g. `p-[13px]`).
### 2.6 Border-radius scale
Calm business UI = soft but not pill-y. Base `--radius` = `0.5rem` (8px), shadcn-style, with the rest derived.
| Token | value | px | Use |
|---|---|---|---|
| `radius-none` | 0 | 0 | tables, full-bleed |
| `radius-sm` | 0.25rem | 4 | badges, inputs (dense), checkboxes |
| `radius-md` | 0.375rem | 6 | buttons, inputs |
| `radius-lg` (`--radius`) | 0.5rem | 8 | cards, popovers, dialogs |
| `radius-xl` | 0.75rem | 12 | large cards, modals |
| `radius-2xl` | 1rem | 16 | feature panels (sparingly) |
| `radius-full` | 9999px | — | avatars, status dots, pills |
shadcn derives `sm/md/lg` from `--radius` via `calc()` — keep that wiring so its components track the base.
### 2.7 Elevation / shadow scale
Business UI leans on **borders over shadows**. Cards on canvas use a 1px border, not a drop shadow. Reserve shadow for genuinely floating layers (dropdown, popover, dialog, toast). Shadows MUST be subtle and neutral (never coloured/glowing).
| Token | Elevation | Value (light) | Use |
|---|---|---|---|
| `shadow-none` | 0 — flat | none | cards, table (use border) |
| `shadow-xs` | 1 — raised | `0 1px 2px rgb(0 0 0 / 0.05)` | subtle raise, sticky header edge |
| `shadow-sm` | 2 — dropdown | `0 1px 3px rgb(0 0 0 / 0.08), 0 1px 2px rgb(0 0 0 / 0.06)` | menu, select, popover, tooltip |
| `shadow-md` | 3 — overlay | `0 4px 6px -1px rgb(0 0 0 / 0.08), 0 2px 4px -2px rgb(0 0 0 / 0.06)` | drawer edge, raised card on hover |
| `shadow-lg` | 4 — modal | `0 10px 15px -3px rgb(0 0 0 / 0.08), 0 4px 6px -4px rgb(0 0 0 / 0.05)` | dialog, command palette |
| `shadow-xl` | 5 — max | `0 20px 25px -5px rgb(0 0 0 / 0.10)` | rare, large overlays |
**Dark mode:** shadows read weakly on dark surfaces — reduce opacity is pointless, instead lean on a lighter **border** (`--color-border`) + a faint `shadow-sm` to separate layers. Elevation in dark = surface-lightening + border, not heavier shadow.
### 2.8 Motion tokens
See §5 for rules. Tokenise durations and easings so nothing is hand-timed.
| Duration token | ms | Use | Easing token | curve | Use |
|---|---|---|---|---|---|
| `duration-fast` | 100 | hover, small state flips | `ease-standard` | `cubic-bezier(0.2, 0, 0, 1)` | default productive motion |
| `duration-base` | 150 | default transitions | `ease-out` | `cubic-bezier(0, 0, 0.2, 1)` | entrances (fade/scale in) |
| `duration-moderate` | 200 | dropdown/popover open | `ease-in` | `cubic-bezier(0.4, 0, 1, 1)` | exits |
| `duration-slow` | 300 | modal, drawer, tab panel | `ease-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | move/resize on screen |
Anything > 300ms is reserved and needs a reason. Curves follow Carbon's "productive" motion (short, functional) rather than "expressive" — RN business UI is productive.
### 2.9 Z-index layers
Named layers prevent the `z-[9999]` arms race. Overlays stack in this fixed order; **tooltip is always on top** (it can appear above a modal).
| Token | value | Layer |
|---|---|---|
| `z-base` | 0 | normal flow |
| `z-raised` | 10 | sticky cells, raised-on-hover |
| `z-dropdown` | 1000 | menus, selects, comboboxes |
| `z-sticky` | 1100 | sticky topbar / table header |
| `z-overlay` | 1200 | dialog/drawer backdrop |
| `z-drawer` | 1300 | side drawer / sheet |
| `z-modal` | 1400 | dialog content |
| `z-popover` | 1500 | popovers over modals |
| `z-toast` | 1600 | toasts / notifications |
| `z-tooltip` | 1700 | tooltips (always top) |
Radix portals render overlays at the end of `<body>`; set these z-values on the portal content so ordering is deterministic regardless of DOM position.
### 2.10 Breakpoints
Mobile-first, Tailwind-aligned. Internal dashboards are desktop-primary but MUST stay usable to 768px; client-facing tools MUST be usable to 360px.
| Token | min-width | Target |
|---|---|---|
| `sm` | 640px | large phone / small tablet |
| `md` | 768px | tablet — sidebar collapses to drawer below this |
| `lg` | 1024px | laptop — primary dashboard breakpoint |
| `xl` | 1280px | desktop |
| `2xl` | 1536px | wide desktop (cap content width; don't let forms stretch) |
Prefer **container queries** (`@container`) for card/panel internals so a component reflows to *its* space, not the viewport — important for dashboard widgets that live in variable-width grid cells.
### 2.11 Wiring tokens into Tailwind + shadcn
1. **Declare primitives + semantics as CSS variables** in a global stylesheet, split by theme:
```css
:root {
/* primitives */
--rn-color-gray-50: oklch(0.985 0 0);
--rn-color-blue-600: oklch(0.55 0.18 255);
/* … full ramps … */
/* semantic (light) — also alias shadcn's names */
--color-bg-surface: var(--rn-color-white);
--color-text-primary: var(--rn-color-gray-900);
--color-border: var(--rn-color-gray-200);
--background: var(--color-bg-canvas);
--foreground: var(--color-text-primary);
--primary: var(--color-interactive-primary);
--ring: var(--color-ring);
--radius: 0.5rem;
}
.dark {
--color-bg-surface: var(--rn-color-gray-900);
--color-text-primary: var(--rn-color-gray-50);
--color-border: var(--rn-color-gray-800);
/* … re-point semantics only … */
}
```
2. **Expose semantics to Tailwind.** Tailwind v4: map in the `@theme` block (`--color-surface: var(--color-bg-surface)` → `bg-surface`). Tailwind v3: `theme.extend.colors = { surface: 'var(--color-bg-surface)', … }`, `theme.extend.spacing`, `borderRadius`, `boxShadow`, `transitionDuration`, `transitionTimingFunction`, `zIndex` all reference the vars. Either way utilities resolve to variables, so a theme flip needs zero component edits.
3. **Keep shadcn's variable names alive.** shadcn components read `--background`, `--primary`, `--muted`, `--ring`, etc. Alias them to RN semantics (step 1) so `npx shadcn add` components drop in already themed.
4. **Dark mode** via the `.dark` class on `<html>` (class strategy), toggled by the theme provider. Never duplicate component markup per theme.
### 2.12 How mature systems do it (references)
- **IBM Carbon** — three-layer tokens (core → system/component), productive vs expressive type & motion, 8px-derived spacing, elevation & motion guidance. https://carbondesignsystem.com/elements/color/overview/ · https://carbondesignsystem.com/guidelines/motion/overview/
- **Shopify Polaris** — semantic/alias colour roles, dense admin UI, tokens as source of truth. https://polaris.shopify.com/tokens/color · https://polaris.shopify.com/design/colors
- **GitHub Primer** — primitives → functional variables, light/dark via CSS variables at the semantic layer. https://primer.style/foundations/primitives · https://primer.style/foundations/color
- **Radix Colors** — 12-step accessible ramps with fixed per-step semantics (backgrounds/borders/solids/text), automatic light/dark. https://www.radix-ui.com/colors
- **shadcn/ui** — CSS-variable theming, OKLCH default, `--radius` derivation, copy-in components on Radix. https://ui.shadcn.com/docs/theming
- **Tailwind theme** — `@theme` / `theme.extend` wiring. https://tailwindcss.com/docs/theme
---
## 3. Typography
### 3.1 Font strategy
Two acceptable options; pick one per app and tokenise as `--font-sans`:
1. **System font stack (default, recommended for internal tools).** Zero network cost, no layout shift, calm and native:
`--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;`
2. **One hosted sans (recommended for client-facing / brand-consistent tools).** **Inter** (variable) — designed for UI legibility at small sizes, excellent number/letter disambiguation. Self-host the variable `.woff2`, `font-display: swap`, subset to Latin. Safe accessible alternative: **IBM Plex Sans** (open, corporate-neutral, wide-language). Do **not** pair more than one display face; RN business UI is single-family.
**Monospace** for IDs, codes, financial figures, and tabular data: `--font-mono: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;`. Use `font-variant-numeric: tabular-nums` on any column of numbers so digits align.
**Pairings (safe, accessible):**
- *Inter (UI + headings) + tabular Inter/JetBrains Mono (data)* — the default recommendation.
- *system-ui (UI) + ui-monospace (data)* — the zero-dependency internal default.
### 3.2 Type scale
Modular scale tuned for **dense business UI**: 16px root (`1rem = 16px`), **14px body** (the workhorse for tables and forms), ~1.2–1.25 ratio for headings. Weights: 400 regular, 500 medium, 600 semibold — **avoid 700+** in calm UI except rare display.
| Role token | rem | px | Weight | Line-height | Use |
|---|---|---|---|---|---|
| `text-display` | 2.25rem | 36 | 600 | 1.15 (41px) | rare page hero / marketing-ish |
| `text-h1` | 1.75rem | 28 | 600 | 1.25 (35px) | page title |
| `text-h2` | 1.375rem | 22 | 600 | 1.3 (29px) | section heading |
| `text-h3` | 1.125rem | 18 | 600 | 1.4 (25px) | card / subsection title |
| `text-body-lg` | 1rem | 16 | 400 | 1.5 (24px) | intro / emphasised body |
| `text-body` | 0.875rem | 14 | 400 | 1.5 (21px) | **default body, table cells, inputs** |
| `text-label` | 0.875rem | 14 | 500 | 1.4 (20px) | form labels, buttons, nav items |
| `text-caption` | 0.75rem | 12 | 400 | 1.4 (17px) | helper text, metadata, timestamps |
| `text-overline` | 0.6875rem | 11 | 600 | 1.3, `letter-spacing: 0.04em`, uppercase | eyebrow / group label |
**Rules:** line-length 45–75ch for prose (`max-w-prose` ≈ 65ch); never centre long text; minimum body 14px (12px only for genuine metadata/captions, never primary content); headings use weight + size for hierarchy, not colour alone.
---
## 4. Component Spec Template & Inventory
### 4.1 The component spec shape
Every component RN builds (or skins from shadcn) is documented against this shape. Keep it terse.
```
Component: <name>
Purpose: one sentence — what task it serves.
Built on: Radix primitive / React Aria / shadcn / custom.
Anatomy: the named parts (e.g. trigger, content, label, icon, indicator).
Variants: visual/intent variants (e.g. primary | secondary | ghost | destructive).
Sizes: sm | md (default) | lg — tie to tokens (height, text, padding).
States: default · hover · focus-visible · active · disabled · loading · error · selected (as applicable).
A11y: role / ARIA, keyboard model, focus behaviour, contrast, target size.
Do / Don't: 1–2 each.
```
**Global a11y baseline every component MUST meet (WCAG 2.2 AA):**
- **Focus-visible ring** on every interactive element: 2px ring in `--color-ring`, 2px offset, ≥ 3:1 contrast vs adjacent. **Never** `outline: none` without a replacement.
- **Keyboard operable**: everything reachable and operable by keyboard in a logical order; `Esc` closes overlays; `Enter`/`Space` activate.
- **Target size** ≥ **24×24px** (2.5.8); primary/touch targets SHOULD be ≥ 44×44px.
- **Labels & names**: every control has an accessible name (visible label, `aria-label`, or `aria-labelledby`); errors linked via `aria-describedby`.
- **State not by colour alone** (1.4.1); disabled controls keep ≥ 3:1 where they must remain perceivable but are exempt from 4.5:1.
- **Reduced motion**: respect `prefers-reduced-motion` (§5).
### 4.2 Core business-app inventory
Each entry lists the 3–5 things that matter most. All "Built on" default to shadcn-on-Radix unless noted.
**Button** — Variants: `primary` (one per view), `secondary`, `outline`, `ghost`, `destructive`, `link`. Sizes: sm(32px)/md(36–40px)/lg(44px), icon-only square. Key states: hover (bg shift via `-hover` token), focus-visible ring, **loading** (spinner + disabled + keep width, don't reflow), disabled (muted, `aria-disabled`). A11y: real `<button>`, name from text or `aria-label` for icon-only, spinner has `aria-live`/`aria-busy`. Don't: two primaries competing; a link styled as a button when it navigates (use `<a>`).
**Input / text field** — Anatomy: label (always visible), input, helper text, error text, optional prefix/suffix/icon. States: default, focus-visible, filled, disabled, **error** (danger border + message), read-only. A11y: `<label for>` (never placeholder-as-label), error via `aria-describedby` + `aria-invalid`, 44px comfortable height. Don't: rely on placeholder for meaning; turn the field red without a text message.
**Select / Combobox** — Select = Radix Select (few, fixed options); **Combobox** = Radix Popover + `cmdk` for search/filter/async (many options). Key: type-ahead, keyboard arrow/Enter/Esc, `aria-expanded`/`role=listbox`/`option`, selected + active-descendant distinct. Loading + empty ("No matches") states for async. Don't: use a native long `<select>` for 50+ options; trap focus.
**Checkbox / Radio / Switch** — Checkbox = multi-select + indeterminate; Radio = one-of-many (group with legend); Switch = **immediate** on/off setting (not for form submit). States: checked/unchecked/indeterminate, focus-visible, disabled. A11y: real inputs / Radix with proper `role`, label clickable and tied, 24px min hit area. Don't: use a switch where a checkbox belongs (switch = takes effect now).
**Form (layout + validation)** — Layout: single column, label above field, `space-4` between fields, `space-8` between sections, actions bottom-right (primary rightmost), max-width ~640px. Validation: validate on blur + on submit (not per-keystroke); **summary of errors at top** linking to fields for long forms; inline error under each field; keep entered data. A11y: focus first invalid field on submit; `aria-live` on the error summary; required marked in text not just `*`. Don't: disable submit to signal invalidity (say why); clear the form on error.
**Table / data-grid** — Built on TanStack Table + shadcn table. Must-haves: **sort** (clickable header, `aria-sort`, visible arrow), **filter** (column/global), **pagination** (see below) or virtualised rows for large sets, **empty state** (§6), row/cell alignment (numbers right + tabular-nums, text left), sticky header (`z-sticky`), optional row selection (checkbox column). Density: comfortable (48px) default, compact (36px) toggle. A11y: real `<table>` semantics, `<th scope>`, caption/summary, keyboard-navigable if grid-interactive (`role=grid`). Don't: horizontal-scroll hidden data with no affordance; convey a status column by colour alone.
**Card** — Anatomy: container (surface bg + 1px border, `radius-lg`, `space-6` padding), optional header (title `text-h3` + actions), body, footer. Elevation: **border, not shadow**, at rest; `shadow-sm` on hover only if interactive. A11y: if the whole card is a link/button, one clear focusable target with an accessible name; don't nest interactive-in-interactive. Don't: card-inside-card-inside-card; drop shadows on every card (visual noise).
**Modal / Dialog** — Built on Radix Dialog. Must: **focus trap**, focus moves to dialog on open and **returns to trigger on close**, `Esc` + backdrop-click close (unless destructive/unsaved-changes guard), `role=dialog` + `aria-labelledby`/`aria-describedby`, backdrop at `z-overlay` + content `z-modal`, scroll-lock body. Size: content max ~`640px`, scroll inside on overflow. Don't: stack modals; use a modal for a non-blocking notice (use toast/inline); auto-focus a destructive button.
**Drawer / Sheet** — Radix Dialog variant sliding from an edge (side for filters/detail, bottom for mobile). Same a11y as dialog (trap, return focus, Esc). Width ~`320–480px`; `z-drawer`. Use for secondary flows (filters, record detail) that keep context; don't use where a full page or inline panel is clearer. Don't: put a required primary flow only in a drawer on mobile.
**Toast / Notification** — Built on shadcn/Sonner. Transient, non-blocking, top-right or bottom-right, `z-toast`, auto-dismiss ~5s (errors persist or last longer + manual dismiss). Variants: success/info/warning/error, each with icon + text (not colour alone). A11y: `role=status` (polite) for info/success, `role=alert` (assertive) for errors; never trap focus; pause-on-hover. Don't: toast for critical errors that need action (use inline/dialog); queue more than ~3 at once.
**Tabs** — Radix Tabs. `role=tablist`/`tab`/`tabpanel`, arrow-key navigation, `aria-selected`, active indicator (underline/pill) with ≥ 3:1 + a non-colour cue. Keep tab count low; don't hide critical actions behind a non-default tab. Don't: use tabs for sequential steps (use a stepper); scroll-hide overflowing tabs with no affordance.
**Accordion** — Radix Accordion. `button` header with `aria-expanded`, `aria-controls`; single- or multi-open; chevron rotates (respect reduced-motion). Use for progressive disclosure of long forms/FAQs. Don't: nest deep; put primary content users always need behind a collapsed panel.
**Tooltip** — Radix Tooltip. Supplementary hint only — **never** the sole source of essential info or a control's label. Keyboard-focus + hover triggers, `z-tooltip`, short delay (~300–500ms open), dismiss on Esc/blur. A11y: `role=tooltip` linked via `aria-describedby`; not focusable itself. Don't: put interactive content inside (use popover); rely on it for anything a user must read to act.
**Badge / Status pill** — Small label for status/category. Status variants map to `status-*` **subtle** bg + solid text + optional dot/icon. Text ≥ 4.5:1 on its fill. A11y: it's text, so screen-reader-readable; if a bare dot conveys status, add visually-hidden text. Don't: encode status by colour alone; make it look clickable if it isn't.
**Breadcrumb** — `nav` with `aria-label="Breadcrumb"`, ordered list, current page `aria-current="page"` (not a link). Show hierarchy for deep apps; collapse middle with an overflow menu on small screens. Don't: use for linear wizard steps; make the last item a link.
**Pagination** — `nav` labelled "Pagination"; prev/next + page numbers (or "Load more"/infinite for feeds); disable + `aria-disabled` prev on page 1; `aria-current="page"` on active; show total/range ("1–20 of 340"). Keyboard operable, ≥ 24px targets. Don't: infinite-scroll a data table users need to scan/compare; hide the total count.
**Navigation (sidebar + topbar)** — Sidebar: `nav` landmark, active item `aria-current="page"` + non-colour cue, groups with headings, collapsible; collapses to a drawer below `md`. Topbar: brand/logo, primary nav or breadcrumbs, search, user menu; sticky at `z-sticky`. A11y: skip-to-content link, keyboard-traversable, visible focus. Don't: hide primary nav behind a hamburger on desktop; more than ~2 nav levels visible at once.
**Skeleton / Loading** — Skeleton placeholders that **match final layout** (same box sizes) to prevent layout shift; subtle shimmer (respect reduced-motion → static). Use for initial data load of a known shape; use a spinner only for indeterminate in-place actions (button loading). A11y: `aria-busy="true"` on the region, `aria-live` announces when loaded. Don't: spinner-only full-page for structured content (skeleton reads calmer); shimmer that pulses aggressively.
**Empty state** — Icon/illustration (simple, on-brand, calm) + one-line explanation + primary action (or "why empty"). Distinguish **no data yet** ("Add your first prospect") from **no results** ("No matches — clear filters"). A11y: real heading + text, action is a real button. Don't: a blank pane; blame the user; hide the way forward.
**Error state** — Inline (field/section) vs page-level (failed load). Say **what happened, why if known, and what to do** in plain language + a **Retry**/recover action; never a raw code alone. `role=alert` for the message; keep the rest of the app usable. Don't: dead-end ("Something went wrong" with no action); lose the user's entered data; blame-y tone.
---
## 5. Motion & Interaction
**Philosophy:** motion clarifies cause-and-effect and spatial relationships; it is never decoration. RN business UI uses **productive** motion — short, functional, forgettable.
**What to animate (SHOULD):**
- State feedback: hover/press colour (`duration-fast`, 100ms), focus ring appearance.
- Enter/exit of overlays: dropdown/popover fade+scale (150–200ms `ease-out` in, `ease-in` out), modal/drawer slide/fade (`duration-slow` 300ms).
- Layout changes: accordion expand, tab panel swap, list add/remove (150–300ms).
- Skeleton shimmer (subtle) and loading spinners.
**Durations:** UI micro-interactions **100–200ms**; larger surfaces (modal, drawer, page) **200–300ms**. **Nothing over 300ms** without a reason. Easing from §2.8 tokens only.
**prefers-reduced-motion (MUST):** wrap non-essential motion; when set, remove transforms/parallax/shimmer and cross-fade or snap instead. Never fully remove *feedback* (a focus ring, a state change) — remove the *movement*, keep the *signal*.
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
```
(Then re-enable essential, reduced-motion-safe transitions explicitly.)
**What NOT to do (MUST NOT):**
- No scroll-jacking, parallax, or scroll-triggered "reveal" choreography in business tools.
- No gratuitous entrance animations on every card/row on load (staggered fly-ins read as consumer-flashy and slow perceived load).
- No glow, neon, pulsing, bouncing, or spring-overshoot on standard controls.
- No auto-playing/looping motion competing with content.
- No animation that delays a user's task (never gate a click behind a 500ms flourish).
- Magic UI / Framer Motion are for *restrained polish only* (a subtle number-ticker on a KPI, a blur-fade on first paint) — not the default texture of the app.
---
## 6. States & Responsive
### 6.1 Mandatory states — every data view
A view that fetches or gates data MUST explicitly handle all of these. Agents: if you build a data view and only render the happy path, it is incomplete.
| State | Trigger | Pattern |
|---|---|---|
| **Loading** | data in flight | Skeleton matching final layout (structured data) or in-place spinner (actions); `aria-busy`. Avoid full-page spinner for known shapes. |
| **Empty** | request succeeded, zero items | Empty state (§4.2): distinguish *no data yet* vs *no results for filters*; offer the next action / clear-filters. |
| **Error** | request failed | Error state (§4.2): plain-language cause + **Retry**; `role=alert`; preserve surrounding UI + entered data. |
| **No-permission** | authorised route, unauthorised data | Explicit "You don't have access" + who to contact / how to request — **not** an empty table or a silent redirect that confuses non-technical users. |
| **Partial / stale** (SHOULD) | some data, background refresh, or degraded | Show what you have + a subtle "updating…" or "some data unavailable" note; never silently show stale data as fresh. |
| **Success** (SHOULD) | action completed | Toast or inline confirmation; then return to the resting state. |
### 6.2 Responsive approach
- **Mobile-first**, breakpoints from §2.10. Internal dashboards are desktop-primary but MUST degrade gracefully to 768px; client-facing tools MUST work to 360px.
- **Dashboards:** CSS Grid with `minmax()` / `auto-fit` so widget cards reflow (e.g. `repeat(auto-fit, minmax(280px, 1fr))`); use **container queries** on widgets so each reflows to its cell, not the viewport. Cap overall content width at `2xl`; don't let a dashboard sprawl edge-to-edge on ultrawide.
- **Forms:** single column always (multi-column forms hurt completion and a11y); max-width ~640px; full-width fields on mobile; sticky action bar on long forms so primary submit stays reachable.
- **Tables:** below `md`, choose one deliberately — horizontal scroll **with a visible affordance** (shadow/edge fade), priority-columns (hide low-priority, keep key columns), or a **card/stacked** layout (each row → a labelled card). Never silently clip columns.
- **Navigation:** sidebar → drawer/hamburger below `md`; keep a persistent bottom or top bar for the 1–2 most-used actions on mobile.
- **Touch:** ≥ 44×44px targets and ≥ `space-2` gaps between them on touch breakpoints (WCAG 2.5.8 floor is 24px; 44px is the comfortable target).
---
## 7. Accessibility — Standard, Scope & Definition of Done
Accessibility is not a §4 footnote — it is a first-class requirement with legal weight for client-facing pharma work. The per-component a11y notes (§4) and the global baseline (§4.1) are the *build* rules; this section is the *standard, the scope, and the gate*.
### 7.1 The operative standard
Target: **WCAG 2.2 Level AA** (W3C Recommendation `REC-WCAG22-20241212`). Building to 2.2 AA satisfies every regime in §7.4 (2.2 is additive to 2.1). Note: **SC 4.1.1 Parsing is obsolete and removed** in 2.2 — validators that still flag it are outdated.
### 7.2 New in WCAG 2.2 that hits business apps (build to these)
| SC | Name | Lvl | What it means for a build |
|---|---|---|---|
| 2.5.8 | Target Size (Minimum) | **AA** | Pointer targets ≥ **24×24 CSS px** (or 24px spacing). Hits icon buttons, table-row actions, pagination, close ✕. |
| 2.4.11 | Focus Not Obscured | **AA** | A focused element must not be hidden behind sticky headers/footers, cookie bars, or your own overlays. |
| 2.5.7 | Dragging Movements | **AA** | Any drag (sliders, reorder, kanban, map pan) needs a single-pointer non-drag alternative. |
| 3.3.8 | Accessible Authentication | **AA** | No cognitive-function test (CAPTCHA/puzzle/transcribe) as the *only* auth path; allow paste, password managers, email link, OAuth, biometrics. |
| 3.3.7 | Redundant Entry | A | Don't force re-entry of info already given in the same process — auto-populate or offer selection (exception: passwords). |
| 3.2.6 | Consistent Help | A | Help affordances (contact, chat, help link) sit in the same relative order across pages. |
| 2.4.13 | Focus Appearance | AAA | Build to it anyway (min area + 3:1) — cheap, de-risks 2.4.7. |
### 7.3 ARIA pattern reference (WAI-ARIA APG) — prefer native HTML first
The component specs in §4 each name their primitive; this is the consolidated keyboard/role contract. Prefer native elements (`<dialog>`, `<details>`, `<input type=range>`, `<button aria-pressed>`) before custom ARIA.
| Component | APG pattern | Key roles/states | Required keyboard |
|---|---|---|---|
| Modal | Dialog (Modal) | `role=dialog` `aria-modal` + labelled | focus in→**trapped**→**restored** to trigger; `Esc` closes; `Tab` cycles |
| Alert | Alert / Alertdialog | `role=alert` (live) / `alertdialog` | alert auto-announces; alertdialog = dialog + focus on action |
| Combobox | Combobox | `role=combobox` `aria-expanded` `aria-controls` `aria-activedescendant` | `Down` open/move, `Enter` select, `Esc` close, type filters |
| Select list | Listbox | `role=listbox` + `option` `aria-selected` | arrows move, `Home/End`, `Space/Enter` select, type-ahead |
| Menu | Menu & Menubar | `role=menu` + `menuitem*`, `aria-haspopup` | arrows, `Enter/Space`, `Esc`, type-ahead |
| Tabs | Tabs | `tablist`/`tab`(`aria-selected`)/`tabpanel`; roving tabindex | `Left/Right` move, `Home/End`, manual/auto activate |
| Accordion | Accordion/Disclosure | header `button aria-expanded aria-controls` | `Enter/Space` toggles; `Tab` between headers |
| Data grid | Grid (Table if static) | `role=grid`/`row`/`gridcell`/`columnheader`; `aria-sort` | arrows move cell, `Home/End`, `Ctrl+Home/End`, `PageUp/Down` |
| Tooltip | Tooltip | `role=tooltip` via `aria-describedby` | hover **and** focus; `Esc` dismiss; satisfies 1.4.13 |
| Switch | Switch | `role=switch` `aria-checked` | `Space` (+`Enter`) toggles |
| Slider | Slider | `role=slider` `aria-valuenow/min/max/text` | arrows + `Home/End` + `PageUp/Down`; needs 2.5.7 alt |
| Breadcrumb | Breadcrumb | `nav[aria-label=Breadcrumb]` `ol`; `aria-current=page` | standard tabbing |
### 7.4 Regulatory scope (verified 2026)
| Regime | Binds | Effective standard |
|---|---|---|
| **EU Accessibility Act** (Dir. 2019/882) | Economic operators placing covered **consumer** products/services on the EU market (e-commerce, e-books, banking, comms, transport, hardware). Obligations **live since 28 June 2025.** | WCAG-derived via EN 301 549. B2B/internal tools generally out of direct scope, **but pharma clients flow the obligation down to the agency** in the MSA/SOW where their product is covered. |
| **EN 301 549** | The EU ICT technical yardstick (procurement + EAA) | Incorporates **WCAG 2.1 A+AA** by reference (v3.2.1); 2.2 alignment pending — verify the version a contract names. |
| **US ADA Title III** | Private "public accommodations", incl. most commercial/healthcare/patient sites | No codified standard; **WCAG 2.1 AA** is the de-facto litigation benchmark. |
| **US Section 508** | Federal agencies + their vendors | **WCAG 2.0 A+AA** (2017 refresh; not yet updated). |
| **US ADA Title II** | State/local government (signals direction) | **WCAG 2.1 AA** (DOJ rule Apr 2024; phased 2026–27). |
| **UK PSBAR 2018 / Equality Act 2010** | UK public sector; general anticipatory duty on private business | **WCAG 2.1 AA** + accessibility statement (public sector); 2.1/2.2 AA the recognised means (Equality Act). |
**Building to WCAG 2.2 AA is the single floor that satisfies all of the above.** EAA-live-since-June-2025 is the item most likely to appear in current EU pharma-client contracts.
### 7.5 VPAT / Accessibility Conformance Report
Pharma procurement + IT-security questionnaires routinely request a **VPAT / ACR** from digital vendors. RN SHOULD be able to produce one per delivered client-facing product — use **VPAT 2.5 INT** (covers WCAG 2.x + Section 508 + EN 301 549 in one). Keep a per-product ACR as a release artefact for T2 tools (ties to `security.md` release gates).
### 7.6 Definition of Done — the accessibility merge gate
Enforced by the **`qa` agent (Playwright + `@axe-core/playwright`)** before merge; automated tooling catches ~30–40% of WCAG, so the scripted keyboard/SR checks below are mandatory, not optional. A change to a user-facing view is not "done" until all pass.
1. **axe-core clean** — 0 violations at `wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22aa` on every changed route/state.
2. **Semantic landmarks** — one `<main>` + `header`/`nav`/`footer`; logical `h1→h6` outline (axe `landmark-*`, `heading-order`).
3. **Keyboard-only pass** — every control reachable/operable; no keyboard trap.
4. **Visible focus** — non-`none` indicator ≥ 3:1 on every focused element.
5. **Modal focus management** — focus in→trapped→`Esc` closes→**returns to trigger**.
6. **Focus not obscured** — focused element not hidden behind sticky/overlay (2.4.11).
7. **Contrast** — text ≥ 4.5:1, large/UI ≥ 3:1 (verified against §2.3 tokens, both themes).
8. **Reduced motion honoured** — non-essential animation gated behind `prefers-reduced-motion` (§5).
9. **Form labels + error association** — programmatic label; `aria-describedby` + `aria-invalid`; error summary `role=alert`.
10. **Status messages announced** — toasts/validation via `aria-live`/`role=status`, no focus theft.
11. **Alt-text discipline** — informative `alt`, decorative `alt=""`, icon-buttons named (axe `image-alt`/`button-name`/`link-name`).
12. **Target size ≥ 24×24px** (or 24px spacing) on icon buttons, row actions, pagination, close.
13. **Drag alternative** exists for any drag interaction (2.5.7).
14. **Reflow** at 320px / 400% zoom — no horizontal scroll, no clipped content.
15. **`<html lang>`** set; unique descriptive `<title>`.
16. **Screen-reader smoke test** on one primary flow (NVDA/VoiceOver or accessibility-tree snapshot) — names/roles/states announced.
17. **200% text zoom** — no loss of content/function.
18. **Accessible auth** — login supports paste + password managers; no CAPTCHA-as-sole-factor (3.3.8).
*Accessibility sources (verified July 2026): W3C WCAG 2.2 `REC-WCAG22-20241212` (w3.org/TR/WCAG22) + "New in 2.2" (w3.org/WAI/standards-guidelines/wcag/new-in-22); WAI-ARIA APG patterns (w3.org/WAI/ARIA/apg/patterns); EU Accessibility Act Dir. 2019/882 (eur-lex.europa.eu) — live 28 Jun 2025; ETSI EN 301 549; US Section 508 (section508.gov) + ADA (ada.gov); UK PSBAR 2018 (legislation.gov.uk/uksi/2018/952); VPAT 2.5 (itic.org/policy/accessibility/vpat); axe-core (@axe-core/playwright).*
---
### Key sources
IBM Carbon Design System — color / type / spacing / motion / elevation (carbondesignsystem.com); Shopify Polaris — tokens + colour roles (polaris.shopify.com/tokens/color); GitHub Primer — primitives + functional colour, light/dark CSS variables (primer.style/foundations); Radix Colors — 12-step accessible ramps (radix-ui.com/colors); Radix Primitives — accessible unstyled components (radix-ui.com/primitives); shadcn/ui — theming, OKLCH, `--radius` (ui.shadcn.com/docs/theming); Tailwind CSS — theme / `@theme` (tailwindcss.com/docs/theme); React Aria — behaviour/a11y hooks (react-spectrum.adobe.com/react-aria); Tremor — dashboard/chart components (tremor.so); TanStack Table (tanstack.com/table); Inter typeface (rsms.me/inter); WCAG 2.2 (w3.org/TR/WCAG22) — 1.4.1 use of colour, 1.4.3 contrast, 1.4.11 non-text contrast, 2.4.7 focus visible, 2.5.8 target size.
Not invented — extracted from the logo, the live site, and our branded work, then extended into full ramps and checked against WCAG contrast maths.
Our brand is red, but red also means “danger,” and the vivid logo red isn't legible enough for text. So the system splits it deliberately:
Crimson carries the brand and passes accessibility for text and buttons; the bright logo red stays on the logo; a distinct scarlet handles errors. And because we never signal anything by colour alone, a delete always carries an icon and the word “Delete” too.
A small team of specialised AIs. The front-end one builds the UI; it is bound to design.md and backed by design tooling.
“The plugin” is our packaged set of AI agents and skills that any RN project can switch on. For UI work, these compose:
Writes and changes the UI — components, pages, styling. Bound to design.md: it builds only from its tokens and component specs. If a request conflicts with the design system, the design system wins.
RN's own checklist for calm, accessible business UI — reuse components, tokens not magic numbers, handle every state, WCAG AA.
Anthropic's design plugin — helps the agent make intentional, non-templated visual choices within our system.
A large library of components, layouts and chart types the agent searches for structure and patterns — style-muzzled, so it never overrides the RN aesthetic.
Drives the finished screen in a real browser, runs an automated accessibility scan, and checks a keyboard pass — a change isn't “done” until it passes (see §5).
They also carry the reference library of production UI repos (Tailwind, shadcn/Radix, Tremor, TanStack Table, React Aria) so the agent builds on proven, accessible foundations rather than reinventing a dropdown.
The same button spec, every time — one primary per view, real focus states, an accessible name, a proper loading state:
This matters commercially, not just ethically.
The whole system targets WCAG 2.2 AA. The palette is contrast-checked, components carry the right keyboard and screen-reader behaviour, and every build must pass an 18-point accessibility gate before it merges — run automatically by the QA agent.
The EU Accessibility Act has been in force since June 2025, and pharma clients increasingly flow accessibility conformance down to their agencies and ask for a formal report (a “VPAT”) during vendor qualification. Building to WCAG 2.2 AA from the first screen means RN can answer that with evidence, not a scramble.
The system needs a designer at the top of it. That's the role — and it's yours to shape.
None of this replaces design work — it depends on it. The AI is very good at building to a spec and very bad at deciding the spec. Your research, wireframes, and information architecture define what we build and where things go; design.md defines how it all looks and behaves; the agent just executes both. Here's the loop:
…then feedback and usage flow back to you, and the system evolves. ↺
design.md — the tokens, the type scale, the component decisions. It's a living document; you're its natural owner.The short of it: this gives your design decisions reach. A choice you make once is applied faithfully across every RN tool, forever, by a builder that never gets tired or cuts a corner on spacing. The design leads; the AI keeps up.
This whole thing is a draft — tell me what's useful, what's confusing, and what you'd want next. It comes straight to me.
Want to go deeper? The full standard is design.md (with a browsable branded version), and it sits alongside our security standard as the two documents every RN tool inherits. Happy to walk you through any part of it, or to sit down and shape the token system and component specs together.
Red Nucleus · Innovation · a companion to design.md and the RN AI development standard · July 2026