Foundations¶
The values every screen is built from: which system a screen belongs to, the storefront's token tiers, and the scales, breakpoints and motion that exist today. The component rules built on these are in Components; the behaviour rules are in UX guidelines.
Two systems¶
| Badger's look | The shop's look | |
|---|---|---|
| Who sees it | The people who run the shop | Shoppers |
| Surfaces | Admin, /system, back-office emails, the operator app, these docs |
Storefront themes (Nova and the themes layered on it), shop emails |
| Tokens | --bx-*, registered in common/src/main/resources/design/badger-brand-tokens.json |
Nova's :root tokens in nova/css/bdgr.css, re-pointed by each theme and by the shop's Design Brief |
| Components | bx- (admin-ui.css / admin-ui.js), reference at /admin/styleguide |
nova-*, shared ones in templates/nova/ui/ |
| Contract | Admin UI components, Badger brand tokens | This page and Components |
The rule that splits them: whatever a shopper sees belongs to the shop and takes its theme.
Whatever Badger says to the people who run the shop looks like Badger. A page a shopper can reach
must not load admin-ui.css, and an admin screen doesn't take the shop's theme (a preview of the
shop, its logo or its product images are content shown in Badger's frame, not styling).
The two systems use the same token tiers with different values. Never copy a value from one into
the other: Badger amber (--bx-brand, #FF8A00) has no place on a storefront, and a shop's
--color-primary has none in the admin.
Storefront token tiers¶
Each tier reads only the one above it.
| Tier | What | Set by | Examples |
|---|---|---|---|
| 1. Base values | The raw palette, fonts, radii, shadows | Nova's defaults, then the theme's stylesheet, then the shop's Design Brief ($cssOverride$/theme.css) and custom CSS (global.css) |
#4F46E5, "Barlow", 4px |
| 2. Semantic tokens | Named roles Nova defines on :root and every Nova-family theme re-points |
nova/css/bdgr.css, then the theme |
--color-primary, --color-surface, --font-family-display, --radius-md |
| 3. Component tokens | Public, optional, one prefix per component, each falling back to a semantic token or the component's default | The theme, on :root or the component's root class |
--pager-item-radius, --nb-cta-bg |
The Design Brief and custom CSS write tier 2 (see Site customisation). Anything styled from tier 2 therefore follows a merchant's brief; anything styled from a hard-coded hex or a theme's private token does not. That is the main reason to keep styling on the tiers.
Nova's semantic tokens¶
All defined on :root at the top of web-mvc/src/main/resources/resources/settBuilder/themes/nova/css/bdgr.css.
| Group | Tokens | Use for |
|---|---|---|
| Brand colours | --color-primary, --color-secondary, --color-accent, each with -dark, -light, -50; --color-primary-ring, --color-primary-tint |
Calls to action, selected states, underlines; -dark for hover, -50 and -tint for pale fills, -ring for focus halos. --color-accent is the rating stars, so it keeps 3:1 on the page |
| Brand text | --color-primary-text, --color-primary-text-hover |
The brand colour as text: links, overlines, tabs, outline buttons. The same as --color-primary and -dark unless the brand colour is too light to read on the page (Depot's yellow, Pop's and Ridge's reds on their tinted pages), when the theme points them at a darker shade. A brief whose primary is too light to read sets them to a darker shade of it |
| On brand | --color-on-primary, --color-on-secondary, --color-on-accent |
The text and icons on a brand fill: --color-on-primary on --color-primary and --color-primary-dark (primary buttons, badges, the selected pager item, filled sections), the other two on secondary and accent fills. White in Nova. Rule: text on a brand fill uses --color-on-*, never a literal white, so a design brief can set it to dark ink when the brand colour is light. A theme whose brand fill needs dark ink (Depot, Spec, Diorama) sets --color-on-primary in its own :root; the brief's stylesheet loads after every theme stylesheet, so its :root value wins. Component tokens keep their fallback chain: var(--button-primary-color, var(--color-on-primary)) |
| Neutrals | --color-gray-50 to --color-gray-950 (11 steps) |
Text (-900 strong, -700 body, -500 muted), borders (-200, -300), sunken fills (-50, -100). Spec and Diorama invert the ramp for their dark look, so pick by role, not by lightness |
| Status | --color-success, --color-warning, --color-error, --color-error-dark, --color-error-ring, --color-info, --color-success-light, --color-error-light; --color-on-success, --color-on-warning, --color-on-error |
Messages, validation, stock and order states, as text on the page (4.5:1). On a status fill (a badge, a sale tag, a remove button) the text is the matching --color-on-*: white, or dark ink where the status colours are light (Spec, Diorama). Never use a status colour for decoration |
| Surface and text | --color-surface, --color-text-strong |
Page and card background; headings and strong text. A dark Design Brief flips these, so read them rather than assuming white and near-black |
| On dark | --color-dark-surface, --color-on-dark, --color-on-dark-accent, --color-on-dark-accent-contrast, and --gradient-brand-dark (emitted by the brief) |
The ground of dark sections, the dark hero presets and the top banner (Nova's --color-gray-950; Spec, whose ramp is inverted, points it at its own dark), and the text, brand colour and button text on it |
| Font families | --font-family-base, --font-family-display |
Body text; headings and display type. Depot also defines --font-family-mono for its own use |
| Font sizes | --font-size-xs (12px) to --font-size-7xl (72px): xs sm base lg xl 2xl 3xl 4xl 5xl 6xl 7xl |
Type scale, rem-based |
| Weights and headings | --font-weight-normal/medium/semibold/bold (400 to 700), --font-weight-heading, --letter-spacing-heading |
Inline emphasis; headings (the brief changes the heading pair without touching inline bold) |
| Line height | --line-height-tight (1.25), -normal (1.5), -relaxed (1.75) |
Headings, body, long-form text |
| Spacing | --space-1 (4px) to --space-32 (128px): 1 2 3 4 5 6 8 10 12 16 20 24 32 |
Padding, gaps, margins on the 4px grid |
| Layout | --container-max-width (1280px), --container-padding (24px, 16px under 768px) |
.nova-container width and side gutter |
| Imagery | --product-image-ratio (1 / 1) |
Product cards and the main gallery; the brief's product image shape sets it |
| Radius | --radius-sm (4px), -md (8px), -lg (12px), -xl (16px), -2xl (24px), -full |
Corners; the brief's corner setting re-points them |
| Shadow | --shadow-xs to --shadow-xl |
Elevation; the brief's shadow depth re-points them |
| Transitions | --transition-fast (150ms), -base (200ms), -slow (300ms), all cubic-bezier(0.4, 0, 0.2, 1) |
Hover and state changes |
Brand colours stay AA. A Design Brief can set any brand colour, so the compiler
(DesignBriefCssCompiler, with the WCAG arithmetic in WcagContrast) keeps every text and
background pair it emits at 4.5:1 or more. It writes --color-on-primary, --color-on-secondary
and --color-on-accent (white, near-black or black, whichever reads on the fill and its -dark
hover shade), a darker --color-primary-text and -hover when the primary is too light to read on
the surface, an --color-on-dark-accent that reaches 4.5:1 on --color-dark-surface, gradient
stops dark enough for white text, and a readable --color-text-strong. The brand colours stay as
fills. So text on a brand fill must read the matching --color-on-* token and brand-coloured text
must read --color-primary-text, never --color-primary or a fixed white, or a merchant's yellow
brand gives white-on-yellow buttons again. What the compiler changed is reported back to the admin
page and the MCP tools (Site customisation).
No theme re-points --space-*, --font-size-*, the plain weights or --radius-full
(Storefront inventory), so these behave as fixed
scales. Every theme re-points the colour ramp, the shadows and five of the six radii.
Component tokens¶
The basket and checkout tokens in nova/fragment/commerceTokens.html are the model: public
--nb-* tokens, each resolved once into an --nbc-* value with a fallback to a semantic token, so
a theme that only re-points Nova's palette and fonts already gets a basket in its own voice, and
one that wants more sets a few --nb-* values. Shared components follow the same pattern with
their own prefix (--pager-*); the rules are in Components.
Mapping a theme's private palette¶
Layered themes keep a private namespace (--b-* Brock, --at-* Atelier, --d-* Depot, --p-*
Pop, --sp-* Spec, --rg-* Ridge, --dio-* Diorama, --bi-* Bistro). That's fine as the theme's
palette, provided it sits below the semantic tier, not beside it:
- Set the semantic tokens first, on
:root, with the theme's raw values (--color-primary, the gray ramp, status colours, font families, radii, shadows). - Derive the private tokens from them wherever a semantic role fits, so the brief still
reaches the theme's own CSS. Depot does this:
--d-ink: var(--color-text-strong),--d-surface: var(--color-surface),--d-signal: var(--color-primary). - Use a raw value in a private token only when no role fits, and say why in a comment (Depot's
--d-on-buy: #15181C, "dark text on the signal colour, whatever the brief sets"). - Re-skin shared components through their tokens, set to semantic or private tokens
(
--pager-item-color: var(--d-ink)). Never restyle a shared component's selectors. - Don't add hex values to rules. A colour written straight into a rule escapes both the brief and the theme's palette.
Scales¶
| Scale | Storefront (Nova) | Admin (bx) |
|---|---|---|
| Spacing | --space-*, 4px grid, 4px to 128px |
--bx-space-1 to --bx-space-8, 4px grid |
| Type | --font-size-xs to -7xl, Inter by default |
--bx-font (Inter), --bx-mono |
| Radius | --radius-sm to -2xl, -full |
--bx-radius-sm/md/lg |
| Shadow | --shadow-xs to -xl |
--bx-shadow-sm/md, --bx-focus-ring |
Use the nearest step rather than a one-off value. Text fields are at least 16px on phones, so iOS doesn't zoom on focus (Nova's product page and donation form follow this).
Breakpoints¶
There are no breakpoint tokens: CSS custom properties can't be used in media queries. Use the values below, which are the ones already in the stylesheets.
| Width | Meaning | Where |
|---|---|---|
max-width: 479px |
Small phones | Pager drops its Previous / Next words; Brock, Pop |
max-width: 767px / min-width: 768px |
Phone / tablet and up | Nova's main split (gutter drops to 16px under 768px); admin tables become cards under 768px |
max-width: 1023px / min-width: 1024px |
Below desktop / desktop | Nova's desktop layouts; every layered theme's header and listing |
min-width: 720px, 900px, 1100px |
Theme-specific steps | Brock-family listings and grids (720), Atelier, Spec, Ridge (900), Pop, Diorama (1100) |
min-width: 992px |
Admin desktop | bx- layouts (rule 3 in Admin UI components) |
| 360px | Narrowest supported width | Admin and storefront must work here with no horizontal scroll |
- Nova writes mobile fallbacks with
max-width; new Nova CSS keeps the pairmax-width: 1023px/min-width: 1024pxfor desktop changes and767px/768pxfor phone changes. - A component that sits in containers of different widths uses a container query, as the admin's
.bx-settingdoes (640px and 560px), not a media query. - Don't add a new breakpoint for one component. If none of the above fits, the layout probably
wants to be fluid (
clamp(),auto-fitgrids).
Motion¶
| What | Where | Duration |
|---|---|---|
| Cross-document view transitions | @view-transition { navigation: auto; } in bdgr.css |
0.3s fade (subtle-fade), 0.2s for a same-page navigation, 0.15s out and 0.25s in on reload |
| Shared-element morphs | view-transition-name on product image, title and price, matched across pages |
Browser default |
| Scroll reveal | .nova-reveal, .nova-stagger, run by js/view-transitions.js |
0.6s and 0.5s, stagger steps of 0.1s |
| Hover and state | --transition-fast/base/slow |
150, 200, 300ms |
- Transitions are cross-document, so every navigation is a fresh page load. Don't re-initialise
anything on
pagereveal(see Performance). - Reduced motion. Under
prefers-reduced-motion: reduce, Nova's stylesheet ("Reduced motion", after the view transitions) cuts every animation and transition to 0.01ms with!important, so every Nova-family theme gets it, motion added later included. It also switches off the hero's fade-up, the cross-document view transitions and shared-element morphs, and shows.nova-revealand.nova-staggercontent in place;js/view-transitions.jsmarks them visible at once instead of on scroll. Delays are not reset, so a delay that isn't motion (a menu waiting before it closes) still works. Brock, Depot, Pop, Spec and Atelier keep their own 1ms blocks, which agree with it. The accessibility check ratchets this: any animation or transition over 100ms still running under reduced motion fails it (Pattern library). Still give new motion its ownprefers-reduced-motion: reducerule (or run it only underno-preference) when near-zero duration isn't enough, such as a marquee that should stop and lay out statically. - Motion explains a change (something arrived, moved or was added). It never carries information on its own, and nothing waits for an animation to finish.