Skip to content
Slab & Sleeve

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. 1

    Museum gallery

    Chrome is dark and desaturated so multi-colored card art is the focal point. No decorative gradients, no glassmorphism.

  2. 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. 3

    Brass discipline

    primary is gold stamping: primary buttons, active navigation, the focus ring, and price. Never status, never a chip at rest.

  4. 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. 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 drift

Branding

The wordmark is Fraunces set in brass, uppercase, tight. The mark is a slab outline with a stamped label.

Slab & Sleeve
Wordmark
<span class="font-display text-display-lg uppercase tracking-tight text-primary">Slab &amp; Sleeve</span>
Slab & Sleeve app icon

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#11131a

    Page canvas (alias of surface).

    --color-background · bg-background

  • surface#11131a

    Level 1: card containers, nav bars, section blocks.

    --color-surface · bg-surface

  • surface-dim#11131a

    Dimmed surface alias.

    --color-surface-dim · bg-surface-dim

  • surface-bright#373941

    Brightest neutral; rare, for maximum lift.

    --color-surface-bright · bg-surface-bright

  • surface-container-lowest#0c0e15

    Level 0: the canvas behind everything, code blocks, empty states.

    --color-surface-container-lowest · bg-surface-container-lowest

  • surface-container-low#191b22

    Hover wash on secondary buttons, inactive chips, info panels.

    --color-surface-container-low · bg-surface-container-low

  • surface-container#1d1f27

    Level 2: hover states, tooltips, modal dialogs.

    --color-surface-container · bg-surface-container

  • surface-container-high#282a31

    Skeleton blocks, table header wash.

    --color-surface-container-high · bg-surface-container-high

  • surface-container-highest#33343c

    Highest container tone (same value as surface-variant).

    --color-surface-container-highest · bg-surface-container-highest

  • surface-variant#33343c

    Variant surface for disabled controls.

    --color-surface-variant · bg-surface-variant

  • surface-tint#ecc246

    Tint source; equals primary.

    --color-surface-tint · bg-surface-tint

  • inverse-surface#e2e2ec

    Light surface for inverted elements.

    --color-inverse-surface · bg-inverse-surface

  • row-alt#14161c

    Zebra 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#e2e2ec

    Body text on the canvas.

    --color-on-background · text-on-background

  • on-surface#e2e2ec

    Primary copy, headings, card names.

    --color-on-surface · text-on-surface

  • on-surface-variant#d1c5af

    Secondary copy, labels, helper text, set names.

    --color-on-surface-variant · text-on-surface-variant

  • inverse-on-surface#2e3038

    Text 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#2a2e3a

    The structural 1px border: containers, tables, dividers.

    --color-hairline · bg-hairline

  • outline#99907b

    Emphasis border: input resting border, secondary buttons.

    --color-outline · bg-outline

  • outline-variant#4d4635

    Warm 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#ecc246

    Primary button fill, active nav, focus ring, price.

    --color-primary · bg-primary

  • on-primary#3d2e00

    Text on a brass button.

    --color-on-primary · bg-on-primary

  • primary-container#c9a227

    Pressed, darker brass container.

    --color-primary-container · bg-primary-container

  • on-primary-container#4b3a00

    Text on primary-container.

    --color-on-primary-container · bg-on-primary-container

  • primary-fixed#ffe08e

    Primary button hover.

    --color-primary-fixed · bg-primary-fixed

  • primary-fixed-dim#ecc246

    Fixed-dim brass (equals primary).

    --color-primary-fixed-dim · bg-primary-fixed-dim

  • on-primary-fixed#241a00

    Text on primary-fixed.

    --color-on-primary-fixed · bg-on-primary-fixed

  • on-primary-fixed-variant#584400

    Low-emphasis text on primary-fixed.

    --color-on-primary-fixed-variant · bg-on-primary-fixed-variant

  • inverse-primary#755b00

    Brass on a light surface.

    --color-inverse-primary · bg-inverse-primary

Secondary (silver)

Cool neutral accents: filter chips, the uncommon rarity pip, muted controls.

  • secondary#c3c6d5

    Silver accent text and icons.

    --color-secondary · bg-secondary

  • on-secondary#2c303c

    Text on a secondary fill.

    --color-on-secondary · bg-on-secondary

  • secondary-container#454956

    Secondary chip fill.

    --color-secondary-container · bg-secondary-container

  • on-secondary-container#b5b8c7

    Text on secondary-container.

    --color-on-secondary-container · bg-on-secondary-container

  • secondary-fixed#dfe2f2

    Lightest silver.

    --color-secondary-fixed · bg-secondary-fixed

  • secondary-fixed-dim#c3c6d5

    Equals secondary.

    --color-secondary-fixed-dim · bg-secondary-fixed-dim

  • on-secondary-fixed#171b27

    Text on secondary-fixed.

    --color-on-secondary-fixed · bg-on-secondary-fixed

  • on-secondary-fixed-variant#434653

    Low-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#b5c4ff

    Pokemon special rarity, info accents.

    --color-tertiary · bg-tertiary

  • on-tertiary#01297a

    Text on a tertiary fill.

    --color-on-tertiary · bg-on-tertiary

  • tertiary-container#89a3f9

    Tertiary chip fill.

    --color-tertiary-container · bg-tertiary-container

  • on-tertiary-container#163686

    Text on tertiary-container.

    --color-on-tertiary-container · bg-on-tertiary-container

  • tertiary-fixed#dbe1ff

    Lightest periwinkle.

    --color-tertiary-fixed · bg-tertiary-fixed

  • tertiary-fixed-dim#b5c4ff

    Equals tertiary.

    --color-tertiary-fixed-dim · bg-tertiary-fixed-dim

  • on-tertiary-fixed#00174d

    Text on tertiary-fixed.

    --color-on-tertiary-fixed · bg-on-tertiary-fixed

  • on-tertiary-fixed-variant#244191

    Low-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#a3e635

    In stock, price up, legal, confirmations.

    --color-success · bg-success

  • warning#eab308

    Low stock (below 5). Never for errors.

    --color-warning · bg-warning

  • danger#ef4444

    Out of stock, price down, every error message.

    --color-danger · bg-danger

  • error#ffb4ab

    Material error role: validation text on error-container.

    --color-error · bg-error

  • on-error#690005

    Text on an error fill.

    --color-on-error · bg-on-error

  • error-container#93000a

    Error banner background.

    --color-error-container · bg-error-container

  • on-error-container#ffdad6

    Text 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#9ca3af

    Common pip.

    --color-rarity-common · bg-rarity-common

  • rarity-uncommon#c3c6d5

    Uncommon pip (reuses secondary silver).

    --color-rarity-uncommon · bg-rarity-uncommon

  • rarity-rare#eab308

    Rare pip (shares warning's value by design).

    --color-rarity-rare · bg-rarity-rare

  • rarity-mythic#f59e0b

    Mythic 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#a3e635

    Near Mint (shares success).

    --color-condition-nm · bg-condition-nm

  • condition-lp#eab308

    Lightly Played.

    --color-condition-lp · bg-condition-lp

  • condition-mp#f59e0b

    Moderately Played.

    --color-condition-mp · bg-condition-mp

  • condition-hp#f97316

    Heavily Played.

    --color-condition-hp · bg-condition-hp

  • condition-dmg#f87171

    Damaged. 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.

SampleForegroundBackgroundWhereRatioLevel
on-surfacesurfaceBody copy, headings14.4:1AAA
on-surface-variantsurfaceSecondary copy, labels10.9:1AAA
on-surface-variantsurface-container-lowInfo panels10.1:1AAA
primarysurfaceBrass links, price, active nav10.9:1AAA
on-primaryprimaryPrimary button text7.8:1AAA
successsurfaceIn-stock, price up12.3:1AAA
warningsurfaceLow-stock pill9.7:1AAA
dangersurfaceErrors, out of stock4.9:1AA
condition-dmgsurfaceDMG condition chip6.7:1AA
tertiarysurfaceSpecial rarity pip label10.9:1AAA
outlinesurfaceInput borders (3:1 component rule)5.9:1AA (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-14px
    Icon-to-label gap, pip offset.
  • unit-28px
    Chip padding, tight stacks.
  • unit-312px
    Button horizontal padding, row padding.
  • unit-416px
    Card padding, mobile page margin.
  • unit-624px
    Desktop gutter, section gaps.
  • unit-832px
    Standard 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-xs2px
  • rounded-sm4px
  • rounded-md6px
  • rounded-lg8px
  • rounded-xl12px
  • rounded-fullpill
  • card-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

Buttons

Two variants, two heights. Primary is brass with dark text; secondary is a hairline outline with brass text. Standard is 32px with uppercase label-caps, prominent is 40px with body text.

<Button>Add to Cart</Button>
<Button size="prominent">Continue to Payment</Button>
<Button variant="secondary">View Details</Button>
<Button variant="secondary" size="prominent">Browse Sets</Button>
<Button disabled>Out of Stock</Button>

States

Hover lifts brass to primary-fixed; secondary washes to surface-container-low. Disabled drops to 50% opacity and keeps its color. Focus is the global 2px brass ring, offset 2px, on every focusable element. Tab into the row above to see it.

.btn        { background: var(--color-primary);       color: var(--color-on-primary); }
.btn:hover  { background: var(--color-primary-fixed); }
:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }  /* never removed */

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

Quantity
1

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.

CommonUncommonRareMythic RareSpecial Rarity
RarityPip
<RarityPip rarity="mythic" showLabel />
NM — Near MintLP — Lightly PlayedMP — Moderately PlayedHP — Heavily PlayedDMG — DamagedNM — Near Mint
ConditionChip · 15% tint + solid border
<ConditionChip condition="NM" />
<ConditionChip condition="NM" active={false} />
Non-FoilFoilReverse HoloEtchedFull Art
FinishChip
<FinishChip finish="foil" />
PSA10 graded 10 by PSABGS9.5 graded 9.5 by BGSCGC8 graded 8 by CGC
GradeBadge · company | grade
<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.

$28.40+4.2%
$22.10-1.1%
$12,500
$78.45-2.4%
PriceDisplay
<PriceDisplay cents={2840} deltaPct={4.2} />
<PriceDisplay cents={7845} deltaPct={-2.4} size="large" />
12 In Stock3 In StockOut of Stock
StockPill
<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

ConditionFinishLangSellerQtyPriceAction
NM — Near MintNon-FoilENSlab & Sleeve4$75.00
NM — Near MintFoilENCardVault1$110.50
LP — Lightly PlayedNon-FoilENDragonHoard2$71.00
HP — Heavily PlayedNon-FoilJATokyoTCG0$52.00
VariantMatrix
<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
<EmptyState icon={SearchX} title="No exact matches" description="…" action={<Button variant="secondary">Clear filters</Button>} />
Loading example
Skeleton · route loading.tsx
<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.

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
Placeholder foil card
Placeholder card back
Placeholder sealed product
CardThumbnail · 5:7, card-corners, alt names the card and set
<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 properties

Use 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; }
Download slab-sleeve-tokens.css