Skip to content

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:

  1. Set the semantic tokens first, on :root, with the theme's raw values (--color-primary, the gray ramp, status colours, font families, radii, shadows).
  2. 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).
  3. 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").
  4. 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.
  5. 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 pair max-width: 1023px / min-width: 1024px for desktop changes and 767px / 768px for phone changes.
  • A component that sits in containers of different widths uses a container query, as the admin's .bx-setting does (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-fit grids).

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-reveal and .nova-stagger content in place; js/view-transitions.js marks 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 own prefers-reduced-motion: reduce rule (or run it only under no-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.