Token contract
Every --slide-* custom property declared in @thebuilder/deckard-core/styles.css, its neutral default, and which part of the runtime reads it.
@thebuilder/deckard-core/styles.css declares every --slide-* token with a neutral default, inside @layer base. A theme redefines them on the canvas and wins, because it ships unlayered CSS and everything in the contract sits in the base layer.
The runtime depends on these names and never on a preset, so a deck with no theme still renders: neutral sizes, app surfaces, no decoration.
@import "tailwindcss";
@import "@thebuilder/deckard-core/styles.css";
Type
| Token | Default | Reads it |
|---|---|---|
--slide-font-heading | var(--font-sans) | Headings and figures |
--slide-font-body | var(--font-sans) | Body copy and the chrome |
--slide-font-mono | var(--font-mono) | Code |
--slide-title-size | 6rem | Hero and breaker headlines |
--slide-heading-size | 4.25rem | The h1 on a content slide |
--slide-subheading-size | 2.625rem | An h2 inside a body |
--slide-lead-size | 2.625rem | The sentence under a headline |
--slide-body-size | 2rem | Bullet copy, the main text of a slide |
--slide-code-size | 1.75rem | Type inside a CodeBlock |
--slide-support-size | 1.75rem | Captions, grid copy, metadata rows |
--slide-label-size | 1.5rem | Eyebrows and other uppercase labels |
--slide-label-tracking | 0.25em | Letter spacing on those labels |
Every size is set against the 1920x1080 canvas at a 16px root, so 1rem is 16 canvas pixels and the scale reads the way it does on a projector rather than the way it does in a browser window. Multiply a default above by 16 to get what it measures on a canvas 1080 pixels tall.
Line height rides along as a unitless multiplier in the blocks, so changing a size token keeps its leading proportional. Never pair a size token with a fixed leading-7.
Spacing
| Token | Default | What it controls |
|---|---|---|
--slide-padding-inline | 7rem | The gutter on both sides of the frame, and inside the canvas header |
--slide-padding-block | 6rem | Top and bottom padding when the slide has no header or footer |
--slide-content-gap | 2.75rem | The rhythm between blocks inside a slide |
--slide-item-gap | 1.625rem | The rhythm between rows inside one block |
--slide-rail-size | 24rem | The fixed side column on HeroSplitSlide and ProseSlide |
The frame those gutters leave is the whole slide, left aligned and uncapped: content starts at the left margin and runs to the right one. Blocks cap their own measure where prose needs it, and anything that wants to be centred centres itself.
Figures
| Token | Default | What it controls |
|---|---|---|
--slide-figure-size | 7.5rem | A StatGrid figure |
--slide-figure-unit-size | 3.5rem | The unit suffix on it |
--slide-meter-size | 0.875rem | The height of the proportion bar under it |
Surfaces
| Token | Default | What it controls |
|---|---|---|
--slide-radius | 0.75rem | Cards, code blocks, small panels |
--slide-radius-lg | 1.25rem | The ContentSlideCard panel |
--slide-surface | var(--card) | The raised surface a panel paints |
--slide-surface-muted | var(--muted) | FeatureGrid cards, secondary panels |
--slide-surface-border | var(--border) | Panel borders, the StatGrid rule |
--slide-surface-shadow | none | Depth under a panel, where the theme wants it |
--slide-accent-soft | color-mix(in oklab, var(--primary) 10%, transparent) | The tint on a CardGrid accent card and a highlighted DataTable row |
Backgrounds
SlideBackground renders one empty element carrying data-slide-background, and the theme paints each variant with these:
| Token | Default | What it controls |
|---|---|---|
--slide-grid-color | transparent | The rule color for the grid variant |
--slide-grid-size | 44px | The grid pitch |
--slide-rule | var(--border) | Hairlines a theme draws on the sheet, such as corner brackets |
--slide-scanline | none | A repeating line mask over the canvas |
--slide-hatch | none | A diagonal fill, which the spotlight variant uses |
--slide-halo | none | The text-shadow a theme puts behind display type |
--slide-motion-color-1 | var(--background) | The ground colour of a motion background |
--slide-motion-color-2 | var(--muted) | The colour its bands and its wash are mixed toward |
--slide-motion-color-3 | var(--accent) | The second mix, which lands on the highlights |
--slide-motion-speed | 1 | How fast the field moves, clamped by the runtime to a usable range |
There is no radial wash, veil, glow or spotlight token. The textures a theme has are flat colour, a rule, a grid, and a hatch, because a radial gradient reads as a smudge on a projector and as grey on paper.
The four --slide-motion-* tokens are read off the canvas by the animated background, and only for a variant the theme names in motion. See Motion backgrounds. A theme sets them per variant and per color mode the way it sets everything else it paints:
.aurora-theme .slide-background[data-slide-background="hero"] {
--slide-motion-color-1: #0a0d1f;
--slide-motion-color-2: #3a2f86;
--slide-motion-color-3: #1c7c96;
background: linear-gradient(160deg, #0a0d1f, #1c2350);
}
That background is what the slide shows while the runtime loads, when the browser has no WebGL, and when the context is lost, so paint a variant you animate as though it never would.
Media
| Token | Default | What it controls |
|---|---|---|
--slide-media-foreground | oklch(1 0 0) | Text over photography |
--slide-media-foreground-muted | oklch(1 0 0 / 85%) | Secondary text over photography |
--slide-media-overlay-subtle | linear-gradient(to top, oklch(0 0 0 / 35%), oklch(0 0 0 / 10%), transparent) | overlay="subtle" |
--slide-media-overlay-medium | linear-gradient(to top, oklch(0 0 0 / 55%), oklch(0 0 0 / 20%), transparent) | overlay="medium" |
--slide-media-overlay-strong | linear-gradient(to top, oklch(0 0 0 / 75%), oklch(0 0 0 / 35%), transparent) | overlay="strong" |
Chrome
The colours, type, and spacing of the header and footer, plus the canvas each is allowed:
| Token | Default | What it controls |
|---|---|---|
--slide-chrome-foreground | var(--muted-foreground) | Header and footer text |
--slide-chrome-emphasis | var(--foreground) | The brand |
--slide-chrome-border | var(--border) | The rules above the footer and below the header |
--slide-chrome-size | var(--slide-label-size) | Chrome type size |
--slide-chrome-tracking | 0.08em | Letter spacing on the counter |
--slide-chrome-gap | 1rem | Column gap inside header and footer |
--slide-progress-track | transparent | The progress element's own background |
--slide-progress-fill | var(--slide-chrome-border) | The filled portion |
--slide-header-space | 8rem | Canvas padding reserved for the header |
--slide-footer-space | 6rem | Canvas padding reserved for the footer |
A theme that redesigns the chrome to a different height sets --slide-header-space and --slide-footer-space, and the slide content moves with it.
Published by the runtime
These are set in the DOM rather than declared in the contract, and a theme or a block reads them:
| Variable | Set on | What it carries |
|---|---|---|
--slide-progress |
[data-slide-progress] |
Position in the deck as a fraction from 0 to 1 |
--slide-chrome-top |
the canvas | --slide-header-space republished as an inset for a bleeding layout |
--slide-chrome-bottom |
the canvas | The same for the footer |
--deckard-scale |
the viewport | The measured fit, applied as a scale() transform |
A fullscreen block pads against the chrome insets so the deck header and slide counter never sit on top of a caption:
className="pb-[calc(3rem+var(--slide-chrome-bottom,0px))]"
Structural attributes
The runtime renders these and the theme styles them. They are as much a part of the contract as the tokens.
| Attribute | On |
|---|---|
data-slide-canvas |
The canvas element, which also carries the theme class |
data-slide-viewport |
The element that scales the canvas |
data-slide-frame |
The slide frame the overflow check measures |
data-slide-background |
The background element, valued with the variant name |
data-slide-motion |
The canvas inside it, valued with the field: aurora, waves, or wash |
data-slide-motion-state |
What that canvas is doing: running, frozen, or unavailable |
data-slide-header, data-slide-footer |
The chrome |
data-slide-header-brand, -title, -meta |
The three header parts |
data-slide-counter, -current, -separator, -total |
The counter |
data-slide-progress |
The progress element, absent entirely when footer.progress is false |
data-slide-surface |
A block that paints its own border or background |
data-slide-panel |
The ContentSlideCard panel |
data-slide-intro |
The eyebrow and heading block above a body |
data-slide-eyebrow |
The uppercase label above a title |
data-slide-title, data-slide-lead |
A slide’s own headline and the sentence under it |
data-slide-kicker |
The label under a FocusSlide body |
data-slide-hero, data-slide-hero-meta |
The opener and its credit row |
data-slide-badge |
The pill above a centered opener’s title |
data-slide-rail, -term, -detail |
The side column on HeroSplitSlide and ProseSlide |
data-slide-breaker, data-slide-breaker-index |
The section divider and the number it carries |
data-slide-accent-rule |
The short rule a minimal breaker or a statement opens on |
data-slide-statement, -text, -source |
The one-sentence layout, its sentence, its attribution |
data-slide-split, -main, -aside |
A CodeSplitSlide and its two sides |
data-slide-note, -index, -text |
One numbered note in that rail |
data-slide-prose, -body, -paragraph |
The label rail layout and its copy |
data-slide-quote, -text, -attribution, -portrait, -name, -source |
A pull quote, down to the portrait |
data-slide-list, -item, -marker, -text |
A BulletList and its three parts |
data-slide-reveal, -item, -item-accent, -title, -description |
A stepped RevealList and its parts |
data-slide-contents, -item, -index, -title, -folio |
A ContentsList row by row |
data-slide-columns, data-slide-column, -index, -label, -title, -text |
A ColumnGrid and one column of it |
data-slide-grid, data-slide-card, -accent, -label, -title, -body |
A FeatureGrid or CardGrid and its cards |
data-slide-media, -item, -frame, -overlay, -caption, -credit |
A media layout and its parts |
data-slide-table, -head, -heading, -row, -highlight, -cell |
A DataTable |
data-slide-timeline, -item, -marker, -done, -date, -label, -detail |
A Timeline and one milestone |
data-slide-log, -row, -tone, -time, -message, -status |
A LogList; -tone is valued ok, note, or alert |
data-stat-grid, -item, -value, -unit, -meter, -caption |
A StatGrid, down to the proportion bar |
These are the other half of the theme contract. Tokens are values; attributes are parts. A theme styles them, decorative ::before and ::after content included, which is how phosphor writes [x] where a list marker goes and ledger hangs a rule there instead, neither of them touching the block. A block may rewrite its markup and its class names freely as long as the attributes stay where they are.
A theme styles parts. It does not add content. A boot log, a status table, a cursor line with words in it are slide content, so they belong in a block the deck composes.
Code blocks are scoped too: [data-slide-canvas] .shiki has its shiki-painted background forced transparent, so a deck never restyles code the surrounding app renders.