Design system · Base properties
Slab & Sleeve Style Guide
A museum-gallery dark interface for a trading card shop: desaturated chrome so the card art is the only saturated thing on screen, hairline borders instead of shadows, brass reserved for the one action that matters, and a 4px grid dense enough to feel like a binder page. Everything below is driven by the same CSS custom properties the live store compiles against.
Overview
Five principles decide almost every styling question before it is asked.
- 1
Museum gallery
Chrome is dark and desaturated so multi-colored card art is the focal point. No decorative gradients, no glassmorphism.
- 2
Hairlines, not shadows
Depth is tonal. A 1px hairline border defines every container; shadows are banned except a sharp 0-spread black on floating modals.
- 3
Brass discipline
primary is gold stamping: primary buttons, active navigation, the focus ring, and price. Never status, never a chip at rest.
- 4
Dense and tabular
12 to 14px body sizes, 32px rows, and tabular numerals on every price and stock count, so columns of money line up like a ledger.
- 5
Color is never alone
Every pip and chip carries a text label or title. Errors are red, warnings amber, and the two are never swapped.
Where the properties live
One definition, three views. The app compiles against the Tailwind v4 @theme block; this page and the downloadable file are checked against it by a unit test.
app/globals.css @theme { --color-primary: #ecc246; … } ← source of truth
public/style-guide/slab-sleeve-tokens.css :root { --color-primary: #ecc246; … } ← drop-in for other themes
lib/style-guide/tokens.ts { name: "primary", hex: "#ecc246", … } ← what this page renders
lib/style-guide/tokens.test.ts fails the build if any of the three driftBranding
The wordmark is Fraunces set in brass, uppercase, tight. The mark is a slab outline with a stamped label.
<span class="font-display text-display-lg uppercase tracking-tight text-primary">Slab & Sleeve</span>Colors
Material-style role names: a surface and its on-surface text, a fill and its on-fill text. Every color on the site is one of these; nothing else is allowed.
Surfaces and elevation
Depth is tonal, never shadowed. The canvas is surface; containers step up one level at a time. Level 0 / 1 / 2 map to surface-container-lowest / surface / surface-container.
background#11131aPage canvas (alias of surface).
--color-background · bg-background
surface#11131aLevel 1: card containers, nav bars, section blocks.
--color-surface · bg-surface
surface-dim#11131aDimmed surface alias.
--color-surface-dim · bg-surface-dim
surface-bright#373941Brightest neutral; rare, for maximum lift.
--color-surface-bright · bg-surface-bright
surface-container-lowest#0c0e15Level 0: the canvas behind everything, code blocks, empty states.
--color-surface-container-lowest · bg-surface-container-lowest
surface-container-low#191b22Hover wash on secondary buttons, inactive chips, info panels.
--color-surface-container-low · bg-surface-container-low
surface-container#1d1f27Level 2: hover states, tooltips, modal dialogs.
--color-surface-container · bg-surface-container
surface-container-high#282a31Skeleton blocks, table header wash.
--color-surface-container-high · bg-surface-container-high
surface-container-highest#33343cHighest container tone (same value as surface-variant).
--color-surface-container-highest · bg-surface-container-highest
surface-variant#33343cVariant surface for disabled controls.
--color-surface-variant · bg-surface-variant
surface-tint#ecc246Tint source; equals primary.
--color-surface-tint · bg-surface-tint
inverse-surface#e2e2ecLight surface for inverted elements.
--color-inverse-surface · bg-inverse-surface
row-alt#14161cZebra striping on dense list rows.
--color-row-alt · bg-row-alt
Text
on-surface for primary copy; on-surface-variant (warm parchment) for secondary copy and labels.
on-background#e2e2ecBody text on the canvas.
--color-on-background · text-on-background
on-surface#e2e2ecPrimary copy, headings, card names.
--color-on-surface · text-on-surface
on-surface-variant#d1c5afSecondary copy, labels, helper text, set names.
--color-on-surface-variant · text-on-surface-variant
inverse-on-surface#2e3038Text on inverse-surface.
--color-inverse-on-surface · text-inverse-on-surface
Outlines
hairline is the structural 1px border on containers, tables, and dividers. outline is the emphasis border on inputs and secondary buttons. outline-variant is the warm separator and the border on neutral chips.
hairline#2a2e3aThe structural 1px border: containers, tables, dividers.
--color-hairline · bg-hairline
outline#99907bEmphasis border: input resting border, secondary buttons.
--color-outline · bg-outline
outline-variant#4d4635Warm separator and the border on neutral chips.
--color-outline-variant · bg-outline-variant
Action brass
The gold stamping on a premium slab. Reserved for primary calls to action, active navigation, the focus ring, and price accents. Never a status color, never on a chip at rest.
primary#ecc246Primary button fill, active nav, focus ring, price.
--color-primary · bg-primary
on-primary#3d2e00Text on a brass button.
--color-on-primary · bg-on-primary
primary-container#c9a227Pressed, darker brass container.
--color-primary-container · bg-primary-container
on-primary-container#4b3a00Text on primary-container.
--color-on-primary-container · bg-on-primary-container
primary-fixed#ffe08ePrimary button hover.
--color-primary-fixed · bg-primary-fixed
primary-fixed-dim#ecc246Fixed-dim brass (equals primary).
--color-primary-fixed-dim · bg-primary-fixed-dim
on-primary-fixed#241a00Text on primary-fixed.
--color-on-primary-fixed · bg-on-primary-fixed
on-primary-fixed-variant#584400Low-emphasis text on primary-fixed.
--color-on-primary-fixed-variant · bg-on-primary-fixed-variant
inverse-primary#755b00Brass on a light surface.
--color-inverse-primary · bg-inverse-primary
Secondary (silver)
Cool neutral accents: filter chips, the uncommon rarity pip, muted controls.
secondary#c3c6d5Silver accent text and icons.
--color-secondary · bg-secondary
on-secondary#2c303cText on a secondary fill.
--color-on-secondary · bg-on-secondary
secondary-container#454956Secondary chip fill.
--color-secondary-container · bg-secondary-container
on-secondary-container#b5b8c7Text on secondary-container.
--color-on-secondary-container · bg-on-secondary-container
secondary-fixed#dfe2f2Lightest silver.
--color-secondary-fixed · bg-secondary-fixed
secondary-fixed-dim#c3c6d5Equals secondary.
--color-secondary-fixed-dim · bg-secondary-fixed-dim
on-secondary-fixed#171b27Text on secondary-fixed.
--color-on-secondary-fixed · bg-on-secondary-fixed
on-secondary-fixed-variant#434653Low-emphasis text on secondary-fixed.
--color-on-secondary-fixed-variant · bg-on-secondary-fixed-variant
Tertiary (periwinkle)
Pokemon special rarities (Holo and above) and informational highlights.
tertiary#b5c4ffPokemon special rarity, info accents.
--color-tertiary · bg-tertiary
on-tertiary#01297aText on a tertiary fill.
--color-on-tertiary · bg-on-tertiary
tertiary-container#89a3f9Tertiary chip fill.
--color-tertiary-container · bg-tertiary-container
on-tertiary-container#163686Text on tertiary-container.
--color-on-tertiary-container · bg-on-tertiary-container
tertiary-fixed#dbe1ffLightest periwinkle.
--color-tertiary-fixed · bg-tertiary-fixed
tertiary-fixed-dim#b5c4ffEquals tertiary.
--color-tertiary-fixed-dim · bg-tertiary-fixed-dim
on-tertiary-fixed#00174dText on tertiary-fixed.
--color-on-tertiary-fixed · bg-on-tertiary-fixed
on-tertiary-fixed-variant#244191Low-emphasis text on tertiary-fixed.
--color-on-tertiary-fixed-variant · bg-on-tertiary-fixed-variant
Status
Stock states, price deltas, form errors, legality chips. Errors always render in danger, never warning. The Material error role is kept for validation surfaces.
success#a3e635In stock, price up, legal, confirmations.
--color-success · bg-success
warning#eab308Low stock (below 5). Never for errors.
--color-warning · bg-warning
danger#ef4444Out of stock, price down, every error message.
--color-danger · bg-danger
error#ffb4abMaterial error role: validation text on error-container.
--color-error · bg-error
on-error#690005Text on an error fill.
--color-on-error · bg-on-error
error-container#93000aError banner background.
--color-error-container · bg-error-container
on-error-container#ffdad6Text on error-container.
--color-on-error-container · bg-on-error-container
Rarity scale
common, uncommon, rare, mythic: 6px pips beside the set name. A pip always carries a text label or title; color is never the sole carrier.
rarity-common#9ca3afCommon pip.
--color-rarity-common · bg-rarity-common
rarity-uncommon#c3c6d5Uncommon pip (reuses secondary silver).
--color-rarity-uncommon · bg-rarity-uncommon
rarity-rare#eab308Rare pip (shares warning's value by design).
--color-rarity-rare · bg-rarity-rare
rarity-mythic#f59e0bMythic pip.
--color-rarity-mythic · bg-rarity-mythic
Condition ramp
NM to DMG runs green to red. Chips tint the background at 15% and keep a solid border of the same color, with the abbreviation in label-caps.
condition-nm#a3e635Near Mint (shares success).
--color-condition-nm · bg-condition-nm
condition-lp#eab308Lightly Played.
--color-condition-lp · bg-condition-lp
condition-mp#f59e0bModerately Played.
--color-condition-mp · bg-condition-mp
condition-hp#f97316Heavily Played.
--color-condition-hp · bg-condition-hp
condition-dmg#f87171Damaged. Lifted from danger's #ef4444 in phase 8 so the tinted chip passes 4.5:1.
--color-condition-dmg · bg-condition-dmg
Using a color
/* CSS */ color: var(--color-primary); background: var(--color-surface-container);
/* Tailwind */ class="text-primary bg-surface-container border-hairline"
/* 15% tint */ class="bg-condition-nm/15 border-condition-nm text-condition-nm"Contrast
WCAG 2.1 AA: 4.5:1 for text, 3:1 for interface components. Ratios below are computed from the token values at render time, not typed in.
| Sample | Foreground | Background | Where | Ratio | Level |
|---|---|---|---|---|---|
| on-surface | surface | Body copy, headings | 14.4:1 | AAA | |
| on-surface-variant | surface | Secondary copy, labels | 10.9:1 | AAA | |
| on-surface-variant | surface-container-low | Info panels | 10.1:1 | AAA | |
| primary | surface | Brass links, price, active nav | 10.9:1 | AAA | |
| on-primary | primary | Primary button text | 7.8:1 | AAA | |
| success | surface | In-stock, price up | 12.3:1 | AAA | |
| warning | surface | Low-stock pill | 9.7:1 | AAA | |
| danger | surface | Errors, out of stock | 4.9:1 | AA | |
| condition-dmg | surface | DMG condition chip | 6.7:1 | AA | |
| tertiary | surface | Special rarity pip label | 10.9:1 | AAA | |
| outline | surface | Input borders (3:1 component rule) | 5.9:1 | AA (3:1) |
Typography
Fraunces (editorial, archival) for names and headers; Inter (functional) for everything with a number in it. Both are self-hosted at build time; the store never calls a font CDN.
Thunderclap Warden
Product names, section headers, branding. Optical size axis enabled.
$1,234.56 · 12 In Stock · NM
All data. Prices and counts always set tabular-nums so columns align.
Type scale
The quick brown fox jumps over the lazy dog
- Token
text-display-lg- Family
- Fraunces
- Size
- 32px / 2rem
- Line
- 1.1
- Weight
- 600
- Track
- -0.02em
- Use
- Page titles, the hero product name.
The quick brown fox jumps over the lazy dog
- Token
text-headline-md- Family
- Fraunces
- Size
- 24px / 1.5rem
- Line
- 1.2
- Weight
- 600
- Use
- Section headers.
The quick brown fox jumps over the lazy dog
- Token
text-headline-sm- Family
- Fraunces
- Size
- 18px / 1.125rem
- Line
- 1.2
- Weight
- 600
- Use
- Card names in grids, panel titles.
The quick brown fox jumps over the lazy dog
- Token
text-body-lg- Family
- Inter
- Size
- 16px / 1rem
- Line
- 1.5
- Weight
- 400
- Use
- Long-form copy, prominent links.
The quick brown fox jumps over the lazy dog
- Token
text-body- Family
- Inter
- Size
- 14px / 0.875rem
- Line
- 1.4
- Weight
- 400
- Use
- Default UI text; the body element uses this.
The quick brown fox jumps over the lazy dog
- Token
text-body-sm- Family
- Inter
- Size
- 12px / 0.75rem
- Line
- 1.4
- Weight
- 400
- Use
- Dense rows, helper text, set names.
The quick brown fox jumps over the lazy dog
- Token
text-label-caps- Family
- Inter
- Size
- 10px / 0.625rem
- Line
- 1
- Weight
- 600
- Track
- 0.05em
- Use
- Chips, pills, form labels, standard buttons. Pair with uppercase.
$12,500.00
- Token
text-price- Family
- Inter
- Size
- 16px / 1rem
- Line
- 1
- Weight
- 600
- Use
- Prices and grades. Always tabular-nums.
Text elements
A section header in Fraunces
Body copy sits in on-surface-variant so headings and prices in on-surface pull forward. Links are brass with a muted underline that brightens on hover.
- Lists use body-sm and the same variant color.
- Never skip a heading level; one h1 per page.
Quotes take the warm outline-variant rule, nothing heavier.
<h2 class="font-display text-headline-md text-on-surface">Section header</h2>
<p class="text-body text-on-surface-variant">Body copy with a <a class="text-primary underline decoration-outline-variant underline-offset-2 hover:decoration-primary">brass link</a>.</p>
<ul class="list-disc pl-5 text-body-sm text-on-surface-variant">…</ul>Layout & spacing
A strict 4px grid. 12 fluid columns up to 1280px with 24px gutters on desktop, 8 columns with 16px gutters on tablet, 4 columns with 16px margins on phones.
Spacing scale
unit-14pxIcon-to-label gap, pip offset.unit-28pxChip padding, tight stacks.unit-312pxButton horizontal padding, row padding.unit-416pxCard padding, mobile page margin.unit-624pxDesktop gutter, section gaps.unit-832pxStandard button height, dense row height.
Container
<div class="mx-auto w-full max-w-7xl px-4 sm:px-6"> /* 1280px max · 16px mobile margin · 24px gutter */Shape & elevation
Soft-technical: rectangles with just enough rounding not to feel sharp. Depth comes from stepping the surface tone, never from a drop shadow.
Radius scale
rounded-xs2pxrounded-sm4pxrounded-md6pxrounded-lg8pxrounded-xl12pxrounded-fullpillcard-corners4% at 5:7
Elevation levels
Three nested tones. Hover and modals take level 2; the canvas behind everything is level 0.
Level 0 · surface-container-lowest
Level 1 · surface
Level 2 · surface-container
Forms
Every field has a visible label. Errors switch the border to danger and link the message with aria-describedby, so the failure is never color-only. Hints share the same mechanism.
We only use this for order updates.
Enter at least two characters.
<TextField label="Email" name="email" type="email" hint="We only use this for order updates." />
<TextField label="Card Name" name="q" error="Enter at least two characters." />Selects and quantity
Chips & pips
Categorical accents, kept small so they never compete with card art. Chips are 20px pills; pips are 6px dots. Every one carries text.
<RarityPip rarity="mythic" showLabel /><ConditionChip condition="NM" />
<ConditionChip condition="NM" active={false} /><FinishChip finish="foil" /><GradeBadge grader="PSA" grade={10} />Price & stock
Money is an integer number of cents everywhere and is formatted in exactly one place. Price deltas are success up, danger down, with an arrow so color is not the only signal. Stock goes warning below five and danger at zero, which also disables the primary action.
<PriceDisplay cents={2840} deltaPct={4.2} />
<PriceDisplay cents={7845} deltaPct={-2.4} size="large" /><StockPill quantity={12} /> <StockPill quantity={3} /> <StockPill quantity={0} />Tables & lists
Dense data is a real table with scoped headers, hairline row dividers, a surface-container-high header wash, and tabular numerals in every money column.
Available Listings
| Condition | Finish | Lang | Seller | Qty | Price | Action |
|---|---|---|---|---|---|---|
| NM — Near Mint | Non-Foil | EN | Slab & Sleeve | 4 | $75.00 | |
| NM — Near Mint | Foil | EN | CardVault | 1 | $110.50 | |
| LP — Lightly Played | Non-Foil | EN | DragonHoard | 2 | $71.00 | |
| HP — Heavily Played | Non-Foil | JA | TokyoTCG | 0 | $52.00 |
<VariantMatrix rows={rows} totalListings={42} />Feedback
Every mutation confirms itself. Toasts for actions, skeletons while a route streams, an empty state that always offers a next step because a search is never a dead end.
Toast
Fired from a plain function after a mutation resolves. Success and error only differ in icon and color; both are announced politely.
import { toast } from "@/components/ui/toast/toast";
toast.success("Listing updated");
toast.error("Could not save the listing");No exact matches
Check the spelling, or browse the recommendations below.
<EmptyState icon={SearchX} title="No exact matches" description="…" action={<Button variant="secondary">Clear filters</Button>} /><div role="status" aria-busy="true">
<span class="sr-only">Loading…</span>
<SkeletonCardGrid count={5} />
</div>Your collection is empty
Add cards you own to track their value over time.
Your card was declined. No charge was made.
Only 3 left at this price.
Order S-83782 confirmed.
Icons & images
Icons are Lucide, inlined as SVG at build time with a 1.5px stroke, sized 16px in rows and 20px in the header. Card images are self-hosted, locked to 5:7 with the 4% corner. Nothing loads from a CDN.
- Search
- ShoppingCart
- User
- Heart
- Package
- ScanLine
- ArrowUp
- ArrowDown
- Check
- X
<CardThumbnail src="/images/cards/…" alt="Thunderclap Warden, Ashes of Verdance" className="w-40" />Code conventions
The rules that keep the CSS itself consistent, the part of a style guide that outlives any one screen.
- Do
- Name colors by role, in pairs: surface / on-surface, primary / on-primary. A name says where a color goes, never what it looks like.
- Define every token once in app/globals.css @theme and consume it as a Tailwind utility (bg-surface, text-primary) or var(--color-…). Hand-written CSS lives only in globals.css.
- Order utilities layout → box → typography → color → state, and keep one component per file under components/ui/.
- Set tabular-nums on anything that holds money or a count, and keep money as integer cents until the single formatter.
- Render errors in danger with the message linked by aria-describedby; confirm every admin mutation with a toast.
- Honor prefers-reduced-motion globally and keep the brass focus ring on every focusable element.
- Don't
- Add a hex value anywhere outside the token set, or a color that is not in DESIGN.md.
- Use brass for status, or put it on a chip or pip at rest.
- Use warning (amber) for an error, or color as the only carrier of meaning.
- Add shadows for depth; step the surface tone instead.
- Load a font, icon, script, or image from a CDN; everything is served from our own origin.
- Build class names dynamically; the Tailwind compiler needs to see every literal.
File structure
app/globals.css @theme tokens · base layer · reduced-motion safety net (the only hand CSS)
components/ui/ Button · TextField · ConditionChip · StockPill · PriceDisplay · Skeleton · toast/ …
components/catalog/ … domain compositions built only from ui/ pieces and token utilities
design/slab_sleeve_boutique/DESIGN.md the design-system source the tokens were ported from
public/style-guide/slab-sleeve-tokens.css the portable copy of the base propertiesUse the properties in your own theme
The whole system is custom properties, so it drops into a theme somebody else wrote. Load the file, then point the theme's selectors at the variables instead of its own hex values.
1 · Load the base properties
<link rel="stylesheet" href="slab-sleeve-tokens.css">
<body class="ss-theme">2 · Point the theme at them
Override the host theme's own rules with the properties. Lower-level helpers (.ss-btn, .ss-input, .ss-chip, .ss-card) are included in the file for themes with no component layer of their own.
/* OpenCart 4 · catalog/view/theme/default/stylesheet/stylesheet.css */
#header, .navbar { background: var(--color-surface); border-bottom: 1px solid var(--color-hairline); }
.btn-primary { background: var(--color-primary); color: var(--color-on-primary); border-radius: var(--radius-md); }
.btn-primary:hover { background: var(--color-primary-fixed); }
.product-thumb .price { font: 600 var(--text-price)/1 var(--font-body); font-variant-numeric: tabular-nums; }
.form-control { border-color: var(--color-outline); background: var(--color-surface); color: var(--color-on-surface); }
.alert-danger { color: var(--color-danger); border-color: var(--color-danger); }
/* WordPress / WooCommerce · functions.php */
wp_enqueue_style('ss-tokens', get_stylesheet_directory_uri() . '/slab-sleeve-tokens.css');3 · Self-host the fonts
The file declares the stacks but ships no @font-face, because the store serves Fraunces and Inter from its own origin and a host theme must do the same. Add the two @font-face rules pointing at your copies, or the stacks fall back to system serif and sans.
@font-face { font-family: "Fraunces"; src: url("/fonts/Fraunces.woff2") format("woff2"); font-display: swap; }
@font-face { font-family: "Inter"; src: url("/fonts/Inter.woff2") format("woff2"); font-display: swap; }