Guides
Theming · design tokens
179 CSS custom properties in three layers, BEM-named. Swap a stylesheet, you swap the brand. No Sass, no JS, no build.
The mental model · three layers
Rikiki's tokens are organised in three layers · two you author in a theme, and the component-scoped overrides you set on one host:
- Palette · raw colors with internal names (
--rik-palette-mango-500,--rik-palette-night-900).Private. Components don't read them; you only touch them when writing a new theme. - Semantic · intent-based names following BEM (
--rik-surface-page,--rik-status-success__border,--rik-accent--strong). This is the public API. - Component-scoped ·
--deck-<tag>-*overrides that default to the matching semantic token. Use them to retheme one instance without touching:root.
The three layers on one change
Take the cover's background. The palette holds the raw colour under a private name, the semantic layer gives it an intent, and the component reads that intent through a token of its own with the semantic value as fallback.
/* LAYER 1 · palette · a raw value, private to the theme */
:root { --rik-palette-night-900: #06080f; }
/* LAYER 2 · semantic · the intent the components read */
:root { --rik-surface-inverse: var(--rik-palette-night-900); }
/* LAYER 3 · component · what deck-cover declares, fallback included
(this line lives in the component, you do not write it) */
background: var(--deck-cover-bg, var(--rik-surface-inverse));Each layer is a different reach. Change--rik-palette-night-900 and every surface built on it moves. Change --rik-surface-inverse and every inverse surface moves, whatever palette entry fed it. Change --deck-cover-bg on one host and only that cover moves · the difference the pair below shows.
<!-- One cover, on that same night background -->
<deck-cover><h1>Inherits the theme</h1></deck-cover>
<!-- The same cover, one value changed at layer 3 -->
<deck-cover style="--deck-cover-bg: #2d1b4e;"><h1>This one only</h1></deck-cover>The preview shows the overridden cover. Its sibling, untouched, would sit on#06080f instead · the value the palette holds and the semantic token forwards.
Swap a whole theme
One stylesheet, one swap. The semantic layer is identical across themes · only the palette values change underneath.
<!-- Default: tropical Rikiki -->
<link rel="stylesheet" href="rikiki/themes/rikiki.css">
<!-- Or: warm-paper Siliceum -->
<link rel="stylesheet" href="rikiki/themes/siliceum.css">
<!-- Or: your own -->
<link rel="stylesheet" href="themes/my-brand.css">Two themes ship with the package:
- rikiki · tropical-jungle palette on a deep navy. Unbounded + Inter (variable axes) + Space Mono via Google Fonts. Default.
- siliceum · warm paper with a gold accent. Source Sans Pro + JetBrains Mono, self-hosted woff2.
Override one or two tokens
Keep the theme but tweak one semantic value. Set the token on :root · all components follow.
:root {
--rik-accent: #ff0066;
--rik-status-success: #00aa44;
}Override one component instance
Every component exposes its own --deck-<tag>-* tokens. Set them inline on the host and they cross the Shadow DOM boundary · regular CSS selectors don't, so this is the supported way to retheme one block.
<deck-card style="
--deck-card-bg: #1a0f2e;
--deck-card-text: #fde9a3;
--deck-card-radius: 4px;
">
<h3>One-off retheming</h3>
<p>Tokens cross the Shadow DOM boundary; CSS selectors don't.</p>
</deck-card>See the Component library for the per-component token list · every component declares its own--deck-<tag>-* family, and the catalogue entry names each one.
Naming convention
BEM with explicit prefixes:
--rik-· framework prefix (avoids collisions with consumer CSS)__element· sub-part inside a block (__bg,__border,__text)--modifier· variant or state (--soft,--strong,--hover,--faint)
No color name in the semantic layer. Names like --yellow, --orange, --green belong to the palette layer only. The semantic API is intent · the palette is implementation.
Reduced motion
Both shipped themes zero out the motion tokens underprefers-reduced-motion: reduce. The spring-ease curve (overshoot 1.8) is neutralised to a standard ease-out. Honouring prefers-reduced-motionis the technique behind WCAG 2.3.3, animation from interactions. If you author your own theme, copy this pattern:
@media (prefers-reduced-motion: reduce) {
:root {
--rik-motion-fast: 0ms;
--rik-motion-base: 0ms;
--rik-motion-slow: 0ms;
--rik-motion__ease-spring: var(--rik-motion__ease-out);
}
}Contrast notes
The brand accent is a display colour, not a body-text colour. The ratios below are measured at build time from the shipped theme files against each theme's own --rik-surface-page, truncated rather than rounded up, and compared with the WCAG AA threshold for body text (4.5:1).
| Token | Theme | On the page surface | AA body text |
|---|---|---|---|
--rik-accent | rikiki | 2.81:1 | fails |
--rik-link | rikiki | 4.49:1 | fails |
--rik-link | siliceum | 4.57:1 | passes |
That is why neither theme uses the raw accent for links: the default theme points --rik-link at --rik-accent--strong and siliceum declares a darker link colour of its own · the table says where each of them lands. If you write a custom theme, measure your--rik-link against your page background; the link CSS in Rikiki uses underlines, so non-text contrast is 3:1 · but body links read as text and should pass AA.
Type scale
Body sizes follow a modular ratio of 1.25 (major third)anchored at 1rem. The new t-shirt aliases (--rik-text-2xsthrough --rik-text-4xl) and the legacy--rik-font-size-* tokens point to the same values.
Note that deck-root sets the document font-size itself, so everyrem scales with the slide rather than with a fixed pixel value: on the default fixed canvas 1rem is 2.35 % of the canvas height (25.4px at the default canvas of 1080), and underfluid it is clamp(14px, 2.35vh, 42px). The injected rule targets adeck-root that is a direct child of body, so an embedded deck (the marketing site is one) leaves the host page'srem alone.
Write a third theme
Re-declare the palette, semantic surfaces, accent and fonts. Everything else inherits from defaults. Drop the file next to rikiki.css, point a <link> at it.
/* themes/my-brand.css · a minimal third theme.
Re-declare only what you want to change · the rest cascades from
whichever theme is loaded BEFORE this one. */
:root {
/* ── LAYER 1 · palette (internal · never consumed by components) ── */
--rik-palette-mango-500: #d97706;
--rik-palette-mango-700: #92400e;
--rik-palette-mango-100: rgba(217, 119, 6, 0.18);
/* ── LAYER 2 · semantic (consumed everywhere) ── */
--rik-surface-page: #f6f4ed;
--rik-surface-raised: #ffffff;
--rik-text-default: #1a1810;
--rik-text-default--muted: #4a4540;
--rik-text-default--faint: #8a8580;
--rik-border-default: #e5e0d4;
--rik-accent: var(--rik-palette-mango-500);
--rik-accent--strong: var(--rik-palette-mango-700);
--rik-accent--soft: var(--rik-palette-mango-100);
--rik-link: var(--rik-accent--strong);
/* Fonts · IBM Plex example */
--rik-font-sans: 'IBM Plex Sans', system-ui, sans-serif;
--rik-font-display: 'IBM Plex Sans Condensed', sans-serif;
--rik-font-mono: 'IBM Plex Mono', ui-monospace, monospace;
}Why tokens, not CSS selectors
Every Rikiki component lives in its own Shadow DOM. Regular CSS selectors don't cross the boundary · deck-cover h1{color: red} from your stylesheet has no effect. CSS custom properties DO inherit through the shadow boundary, which is why the entire override surface is tokens-only.
Token reference
The full semantic-layer surface, grouped by domain:
/* SEMANTIC LAYER · consumer-facing
* Naming · --rik-<block>__<element>--<modifier> (BEM).
* Components consume these · NOT --rik-palette-* (those are private). */
/* Surfaces */
--rik-surface-page / --rik-surface-raised / --rik-surface-raised--strong
--rik-surface-sunken / --rik-surface-tint / --rik-surface-tint--strong
--rik-surface-inverse / --rik-surface-inverse--soft / --rik-surface-inverse__overlay
/* Text */
--rik-text-default / --rik-text-default--muted / --rik-text-default--faint
--rik-text-inverse / --rik-text-inverse--muted / --rik-text-inverse--faint / --rik-text-inverse--ghost
/* Borders */
--rik-border-default / --rik-border-default--subtle / --rik-border-inverse
/* Accent */
--rik-accent / --rik-accent--soft / --rik-accent--faint / --rik-accent--strong / --rik-accent__on
/* Status (success / danger / warn / info) */
--rik-status-success / --rik-status-success__bg / --rik-status-success__border
--rik-status-danger / --rik-status-danger__bg / --rik-status-danger__border
--rik-status-warn / --rik-status-warn__bg / --rik-status-warn__border
--rik-status-info / __bg / __bg--mid / __bg--strong / __border / __text
/* Interactive */
--rik-interactive-bg / --rik-interactive-bg--hover / --rik-interactive-bg--active / --rik-interactive-bg--selected
--rik-interactive-fg / --rik-interactive-fg--hover
/* Focus / link / selection */
--rik-focus-ring / --rik-focus-ring--width / --rik-focus-ring--offset
--rik-link / --rik-link--hover / --rik-link--visited
--rik-selection__bg / --rik-selection__text
/* Decorative palette (when you need a specific hue) */
--rik-decor-orchid / --rik-decor-lime / --rik-decor-canary
/* Elevation, motion, z-index */
--rik-elevation-{1,2,3}
--rik-motion-{fast,base,slow} / --rik-motion__ease-{out,in-out,spring}
--rik-z-{base,sticky,overlay,modal,toast,tooltip}
/* Typography · modular scale 1.25 + legacy aliases */
--rik-font-sans / --rik-font-display / --rik-font-mono
--rik-text-{2xs,xs,sm,base,md,lg,xl,2xl,3xl,4xl}
--rik-font-size-{display,section,h1,h2,lead,body,sm,xs,mono,mono-sm,strong,big,mega,hook,stat}
/* Space / radius / icon */
--rik-space-{hair,2xs,1,2,3,4,5,6}
--rik-radius-{xs,sm,md,lg,pill}
--rik-icon-{xs,sm,md,lg,xl,2xl}
/* Slide chrome */
--rik-slide-padding-y / --rik-slide-padding-x / --rik-title-block
/* Code surface · used by deck-code */
--rik-code__bg / --rik-code__border / --rik-code__text
--rik-code__syntax-{keyword,string,number,comment,type,property,function}Next
- Component library · the
--deck-<tag>-*family each component declares. - Lit and Shadow DOM · why the override surface is tokens rather than selectors.