Guides
Theming · design tokens
~140 CSS custom properties, two layers, BEM-named. Swap a stylesheet, you swap the brand. No Sass, no JS, no build.
The mental model · two layers
Rikiki's tokens are organised in two layers (three if you count component-scoped overrides):
- 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.
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 · 9–12 tokens per component on average).
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 under
prefers-reduced-motion: reduce. The spring-ease curve (overshoot
1.8) is neutralised to a standard ease-out. WCAG 2.3.3 compliance, vestibular
safety. 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 #f07020 on paper is 3.2:1,
which fails WCAG AA for body text. Both themes route --rik-link
through --rik-accent--strong (a darker shade that passes 4.5:1).
If you write a custom theme, audit 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-2xs
through --rik-text-4xl) and the legacy
--rik-font-size-* tokens point to the same values.
Note that the slide root applies html { font-size: clamp(14px, 2.35vh, 42px) }
so every rem scales with the viewport height. That's a deliberate
choice for slides; if you embed Rikiki in a regular page (the marketing site
does), drop that rule from your own stylesheet.
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.
Concretely: every component declares its custom property reads with the
semantic fallback, e.g.
background: var(--deck-cover-bg, var(--rik-surface-inverse)).
Override --deck-cover-bg for one instance, override
--rik-surface-inverse for all of them.
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}