Extracted from: Token-Architecture.pdf, A-Shared-Language-for-Design-and-Engineering.pdf, Design-System-Guide.pdf, Component-Catalogue.pdf — DEPARTMNT UK Ltd, Brandon Eddy, v2.0, 2026-02-03
Every component, variant, size, and state follows one scannable pattern:
{component}/{variant}/{size}/{state}
Examples:
button/primary/md/defaultbutton/secondary/sm/disabledinput/text/md/erroravatar/circle/lg/defaultbadge/success/sm/defaultThe goal is a "barcode" — a designer and a developer should be able to map between Figma and code instantly, with zero interpretation.
PascalCase in code. Plain noun in Figma. No suffixes like "Component" or "Element".
| Figma name | Code name |
|---|---|
| Button | Button |
| Input | Input |
| Card | Card |
| Avatar | Avatar |
| Badge | Badge |
| Checkbox | Checkbox |
| Switch | Switch |
| Select | Select |
| Dialog | Dialog |
| Tooltip | Tooltip |
| Tabs | Tabs |
| Accordion | Accordion |
| Toast | Toast |
Lowercase. Describes visual style, not hierarchy.
Preferred variant vocabulary:
primary secondary ghost destructive outline soliddefault success warning error infoelevated outlined filledDo not use 1, A, blue, or brand names as variant values.
Always use words, never numbers as prop values.
| Token | px value |
|---|---|
xs |
24px |
sm |
32px |
md |
40px |
lg |
48px |
xl |
64px |
Components may not support every size — document which sizes a component accepts in its props type definition.
Four states are expressed as props (code handles them):
default — no prop needed, implicitdisabled — disabled boolean proploading — loading boolean properror — error string prop (carries the message)Three states are NOT expressed as props (CSS handles them):
hover — :hover pseudo-class onlyfocus — :focus-visible pseudo-class onlyactive — :active pseudo-class onlyNever create Figma variants or React props for hover, focus, or active.
Figma layer names for Radix-based components must exactly match the Radix anatomy. This is a hard contract — no synonyms, no paraphrasing.
| Radix component | Required layer names |
|---|---|
| Dialog | Overlay Content Close Title Description Body |
| AlertDialog | Overlay Content Title Description Cancel Action |
| Select | Trigger Value Icon Content Viewport Item Separator |
| Tooltip | Trigger Content Arrow |
| Tabs | List Trigger Content |
| Accordion | Item Header Trigger Content |
| Toast | Root Title Description Action Close Viewport |
| Popover | Trigger Content Arrow Close Body |
| DropdownMenu | Trigger Content Label Item Separator |
| Avatar | Image Fallback |
| Checkbox | Root Indicator |
| Switch | Root Thumb |
A colour profile decides where a brand's accent lands in the UI. Each brand declares one
at Tier 2 via colorProfile, and DepartmntTheme stamps it on the themed subtree as
data-color-profile, which the generated DEPARTMNT layer reads.
colorProfile |
Display label | Accent paints… | How it's applied |
|---|---|---|---|
default |
Default (action / accent) | Interactive controls — buttons, links, states | Radix Themes' native behaviour (no override) |
inverted |
Inverted (chrome / surface) | Structural frame — panels, surfaces, chrome | [data-color-profile='inverted'] .radix-themes repaints --color-panel-solid / --color-surface with the accent scale |
custom |
Custom (saved profile) | A per-role mapping you define | Per-role --var: var(--source) overrides set on the Theme element |
The two named slots Chrome / Surface and Action / Accent still exist as Tier-2 tokens
(--brand-chrome-bg/-fg, --brand-action-bg/-fg) describing each region; the profile chooses
which one the accent flows into.
Rules:
colorProfile in tokens.json; default is default.inverted is for brands like H.LLS where the signature colour is the surface (orange chrome
on a dark page), not the buttons.custom is authored in Portal Design Studio's Theme Lab (tiered token dropdown per role) and
saved as a reusable mapping; use it to design a mapping before baking it into a brand.Domain prefix + PascalCase name. Domain is always lowercase.
{domain}/{ComponentName}
Examples:
auth/LoginFormauth/RegisterFormdashboard/StatsWidgetproduct/ProductCardcheckout/PaymentCardshared/Headershared/Footershared/SidebarModule components are suffixed Module (HeroModule, EventModule) and live in /modules.
Each is built on Radix Themes + our token layer and laid out with auto layout (Flex/Grid,
fluid widths, no fixed pixel frames) so it reflows at any container size.
Every module is one of two classes. The distinction is behaviour, not visual weight.
| Class | What it is | Client effort | Examples |
|---|---|---|---|
| Standard | Mostly static content modules — a client just fills in copy/media and posts it. No data wiring, no internal state. | Post-and-go | HeroModule, RichTextModule, GalleryModule, QuoteModule |
| Advanced | Complex modules with additional functionality — data, state, or commerce behaviour (tickets, carts, feeds). | Configured / connected | EventModule, ProductModule, FeedModule, TicketModule |
Rule of thumb: if a content editor can fully use it by typing text and dropping an image, it's
Standard. If it needs data, options, or wired actions to function, it's Advanced. The
class is recorded as tier: 'standard' | 'advanced' in studio/lib/modules.ts and drives the
Standard/Advanced tabs in Portal Design Studio → Module Preview.
Number + flow + screen name + device.
{NN}_{flow}/{screen}/{device}
Examples:
01_onboarding/splash/mobile02_auth/login/mobile02_auth/login/desktop03_dashboard/home/desktop| Thing | Convention | Example |
|---|---|---|
| Component folder | PascalCase | Button/ ProductCard/ |
| Component file | PascalCase | Button.tsx |
| CSS Module | PascalCase + .module.css |
Button.module.css |
| Barrel export | lowercase | index.ts |
| Hook | camelCase with use prefix |
useTheme.ts |
| Utility | camelCase | cn.ts formatDate.ts |
| Token file | kebab-case | tokens.json tokens.css |
| Page routes | kebab-case | reset-password/ |
camelCase within CSS Modules. No BEM, no utility prefixes.
/* correct */
.button {}
.buttonPrimary {}
.loadingSpinner {}
/* incorrect */
.Button {}
.button--primary {}
.loading-spinner {}
.btn {}
Three-tier structure. See /tokens for the full reference.
--{tier}-{category}-{scale}
Tier 1 (global): --global-spacing-4
Tier 2 (brand): --brand-color-primary
Tier 3 (component): --button-bg, --button-padding-x
Components reference Tier 3 only. Never reference Tier 1 directly inside a component file.
The brand tier is also mapped onto Radix Themes' variables (--accent-1…12, --gray-1…12,
contrast/surface/indicator/track) so stock Radix Themes components pick up the brand
automatically. This mapping is generated — never hand-edit Radix variables.
This system is Radix Themes-first. Build every component at the lowest-effort tier that achieves the design — default to Tier 1, escalate only when the design demands it. See it demonstrated live in Portal Design Studio → Theme Lab.
| Tier | When | Mechanism |
|---|---|---|
| 1 — Token override | Standard component, brand restyle | Override Radix Themes CSS variables per [data-brand] (accent/gray scale, radius, fonts) |
| 2 — Wrap + CSS Module | A Themes component needs visual tweaks | Render the Themes component, pass className, layer a CSS Module |
| 3 — Headless + CSS Module | Bespoke / signature component | Build on a headless @radix-ui/react-* primitive, style fully from tokens |
Apply a brand with the <DepartmntTheme> provider (wraps Radix <Theme>, maps the brand to
its appearance, and scopes the brand's tokens). Standard layout/typography comes from Radix
Themes (Box, Flex, Grid, Text, Heading) — /primitives is for bespoke base elements only.
Components appear in Portal Design Studio's Lookbook via a hybrid convention:
variant × size × state from the prop
union types. No extra file needed.<Name>.preview.tsx — add one only when a component needs bespoke setup
(specific children, context, or data) that auto-extraction can't capture.These names apply only to the internal Studio app (not the published @departmnt/ui library).
The Studio shell is a three-region layout:
| Region | Where | What it holds |
|---|---|---|
| Nav Rail | Left shell <aside> |
Wordmark, brand switcher, and the view navigation (Foundations, Theme Lab, …). |
| Main | Centre, between the rails | The active view's content (e.g. the Module Preview device frame). |
| Control Rail | Right shell <aside> |
The global Appearance Toggle plus the active view's contextual controls/info, which each view pushes up to the shell via the useRail() hook. |
@radix-ui/react-switch with a stateful
sun/moon knob icon) that lives at the top of the Control Rail and drives the shared appearance
context consumed by the preview views.<aside> elements,
sticky and full height. View-specific panels (Theme Lab options, Module Preview module info)
render inside the Control Rail via useRail(), never as their own page-level column.