Themes.
Every Aktion component is already wired to a set of 80+ design
tokens — colors, radii, spacing, type, shadows,
gradients, z‑index layers, and motion. Pick one of
seven built‑in themes with a single attribute, or
brand your own by overriding tokens in‑program with
$theme({…}). No CSS files, no
stylesheet plumbing — just a token map.
How theming works
A theme is nothing more than a bundle of CSS custom properties
(--rui-color-primary, --rui-radius-md, …) written
onto the <aktion-app> host element. Every component reads
those variables, so changing a token reskins the whole tree instantly —
without re‑rendering. There are two layers, and they compose:
| Layer | Where it lives | Token shape |
|---|---|---|
| Host base | theme attribute / setTheme() on the element |
A built‑in name ("dark") or a flat token map ({ colorPrimary: "…" }) |
| Per‑response overlay | A bare $theme({…}) inside the program |
Grouped tokens ({ colors: { primary } }) layered on top of the base |
Two token shapes, one system
The host paths (theme attribute, setTheme()) take
flat keys like colorPrimary / radiusMd.
The in‑program $theme({…}) takes grouped
keys like colors.primary / radius.md. Both end up as the
same --rui-* CSS variables — pick the shape that matches where
you're setting the theme.
Built-in themes
Six themes come in the box. Select one with the theme
attribute on the host element — light is the default if
you omit it. Switching is instant: only the host's CSS variables change.
<aktion-app theme="dark"></aktion-app>
| Theme | Feel |
|---|---|
light | Default. Crisp white surfaces, indigo accent, 1px borders. |
dark | High‑contrast dark surfaces, soft indigo accent. |
corporate | Cloud‑console — deep navy + cyan, dense layout, small radii. |
soft | Soft & rounded — lavender + mint, large radii, gentle shadows. |
glass | Light glassmorphism — frosted white surfaces with backdrop blur over an airy pastel gradient. |
modern | Modern SaaS — light canvas, generous rounding, ink primary with pill buttons, soft shadows, vibrant charts. |
Set the theme imperatively too — handy for a user‑facing theme switcher in the host page:
const app = document.querySelector("aktion-app");
app.setTheme("dark"); // a built-in name
app.setTheme({ colorPrimary: "#7c3aed" }); // flat token overrides
This docs site has its own light/dark toggle in the top bar — the live demos below follow it, so you can see how the same tokens read on either background.
Branding in‑program with $theme({…})
Inside a program, write a bare $theme({…}) statement
(anywhere at the top level, alongside your $app(…) root).
The runtime writes those tokens to the host and reskins the UI instantly.
Tokens live in groups: override just
colors.primary and every button, badge, link, and focus ring
follows.
$theme({ colors: { primary: "#7c3aed", primaryHover: "#6d28d9" } })
$app(Column([
Card([
CardHeader("Purple brand", { subtitle: "Only colors.primary changed" }),
Row([
Button("Primary", { variant: "primary" }),
Button("Ghost", { variant: "ghost" }),
Badge("New", { tone: "primary" })
], { gap: "sm" })
])
], { gap: "md" }))
Combine groups. Here colors plus radius give a
mint‑accented look with a bigger card radius and fully‑rounded
buttons:
$theme({
colors: { primary: "#0d9488", primaryHover: "#0f766e", accent: "#f59e0b" },
radius: { md: "18px", button: "999px" }
})
$app(Column([
Card([
CardHeader("Mint & rounded"),
Text("Bigger card radius, pill-shaped buttons."),
Row([
Button("Save", { variant: "primary" }),
Button("Cancel", { variant: "secondary" }),
Badge("Beta", { tone: "warning" })
], { gap: "sm" })
])
], { gap: "md" }))
Typography is a group too. Override font.family and
font.familyHeading for a different voice — here a serif
editorial look with a crimson accent and sharp corners:
$theme({
colors: { primary: "#dc2626", primaryHover: "#b91c1c" },
font: { family: "Georgia, 'Times New Roman', serif", familyHeading: "Georgia, serif" },
radius: { md: "2px", button: "2px" }
})
$app(Column([
Card([
CardHeader("Editorial", { subtitle: "Serif type, crimson accent, sharp corners" }),
Text("$theme({...}) overrides only the tokens you pass — everything else inherits the host theme."),
Row([Button("Read more", { variant: "primary" }), Badge("Featured", { tone: "danger" })], { gap: "sm" })
])
], { gap: "md" }))
Token group reference
Inside $theme({…}) the top‑level keys are
structured groups. Each group's camel‑case keys flatten
to a predictable CSS variable: colors.primary →
--rui-color-primary, radius.md →
--rui-radius-md, font.familyHeading →
--rui-font-family-heading. Here is every group:
| Group | CSS variable | Keys |
|---|---|---|
colors | --rui-color-* | See the colour table below (19 tokens). |
radius | --rui-radius-* | xs · sm · md · lg · pill · button · input |
spacing | --rui-spacing-* | xs · sm · md · lg · xl · 2xl · 3xl |
font | --rui-font-* | family · familyHeading · familyMono · sizeBase · sizeSm · sizeLg · sizeHeading · sizeTitle · weightBody · weightHeading |
shadows | --rui-shadow-* | sm · md · lg |
gradients | --rui-gradient-* | brand · accent · warm · cool · success · danger |
zIndex | --rui-z-* | base · raised · dropdown · sticky · banner · overlay · modal · popover · toast · tooltip |
motion | --rui-motion-* | fast · base · slow · ease |
fonts | — | import: ["Inter:400,700"] — web‑font loader (side effect, not a variable). |
icons | — | { name: "<svg…>" } — register inline SVG icons (side effect). |
Two metadata keys are accepted at the top level: name selects one
of the seven built‑in themes as the base palette before your overrides
layer on top, and direction ("ltr"/"rtl")
is advisory metadata. Neither is emitted as a CSS variable. Flat keys
(colorPrimary) and free‑form CSS variables are flagged by the
schema validator and dropped — inside $theme({…}) always
use the grouped shape.
Colour tokens
The colors group is the one you'll reach for most. Defaults shown
are from the light theme:
| Key | CSS variable | Default (light) | Role |
|---|---|---|---|
bg | --rui-color-bg | #ffffff | Page background. |
bgSubtle | --rui-color-bg-subtle | #f8fafc | Subtle page wash. |
surface | --rui-color-surface | #ffffff | Cards, sheets, panels. |
surfaceMuted | --rui-color-surface-muted | #f1f5f9 | Inset / secondary surfaces. |
border | --rui-color-border | #e2e8f0 | Default 1px borders. |
borderSubtle | --rui-color-border-subtle | rgba(15,23,42,.08) | Hairline dividers. |
text | --rui-color-text | #0f172a | Primary body text. |
textMuted | --rui-color-text-muted | #475569 | Secondary / caption text. |
primary | --rui-color-primary | #6366f1 | Brand colour — primary buttons, active states. |
primaryHover | --rui-color-primary-hover | #4f46e5 | Hover/pressed brand colour. |
primaryText | --rui-color-primary-text | #ffffff | Text/icon on a primary fill. |
accent | --rui-color-accent | #6366f1 | Secondary accent — links, chips, callouts. |
accentHover | --rui-color-accent-hover | #4f46e5 | Hover accent. |
accentText | --rui-color-accent-text | #ffffff | Text on an accent fill. |
focusRing | --rui-color-focus-ring | #6366f1 | Keyboard focus outline. |
success | --rui-color-success | #10b981 | Positive status. |
warning | --rui-color-warning | #f59e0b | Caution status. |
danger | --rui-color-danger | #ef4444 | Error / destructive status. |
info | --rui-color-info | #06b6d4 | Informational status. |
Radius, spacing, type & shadows
These shape and rhythm tokens drive corner rounding, the spacing scale behind
gap/padding props and sx, the type ramp, and
elevation. Defaults are from the light theme.
| Group | Key → default |
|---|---|
radius | xs 4px · sm 6px · md 10px · lg 16px · pill 999px · button 6px · input 6px |
spacing | xs 4px · sm 8px · md 12px · lg 20px · xl 32px · 2xl 48px · 3xl 80px |
font sizes | sizeBase 14px · sizeSm 12px · sizeLg 16px · sizeHeading 16px · sizeTitle 22px |
font weights | weightBody 400 · weightHeading 700 |
font families | family / familyHeading system‑ui stack · familyMono ui‑monospace stack |
shadows | sm subtle · md medium · lg large (each a full box-shadow string) |
The spacing scale advertises t‑shirt names (sm/md/lg)
but stores them as the short s/m/l CSS variables, so both
spacing: { md: "16px" } and the legacy { m: "16px" } set the same
--rui-spacing-m.
Gradients, fonts & custom icons
Three groups go beyond colors and radii. Gradients are named
and reusable — each value is an array of color stops, an object
{ stops, angle }, or a safe gradient string — and they power
GradientText and the sx.bg: "gradient.brand" intent.
Fonts load on demand from a shorthand list.
Icons register inline SVG under a name you can use anywhere an
icon name is accepted.
$theme({
gradients: { brand: ["#6366f1", "#ec4899"], cool: ["#0ea5e9", "#22d3ee"] },
icons: { spark: "<svg viewBox='0 0 24 24'><path d='M12 2l2 7 7 2-7 2-2 7-2-7-7-2 7-2z'/></svg>" }
})
$app(Column([
GradientText("Brandable gradients", { gradient: "brand", variant: "display" }),
Row([
Button("Custom icon", { icon: "spark", variant: "primary" }),
Badge("New", { tone: "primary" })
], { gap: "sm" }),
Card([Text("sx gradient background")], { sx: { bg: "gradient.cool", p: "l", color: "#fff" } })
], { gap: "md" }))
Web fonts load on demand: add a fonts group with an
import list of "Family:weights" shorthands. Aktion injects
one sanitised Google Fonts <link>, after which the family is
available to font.family:
$theme({
fonts: { import: ["Inter:400,600,700"] },
font: { family: "Inter, system-ui, sans-serif" }
})
Custom icons register inline SVG under a name. Once registered,
that name works anywhere an icon name is accepted — the icon prop
on Button, Icon("spark"), list bullets, and so on:
$theme({ icons: { spark: "<svg viewBox='0 0 24 24'><path d='M12 2l2 7 7 2-7 2-2 7-2-7-7-2 7-2z'/></svg>" } })
Using theme tokens in your UI
You rarely touch raw colors — components already speak the active theme. When
you do need to style something directly, reference tokens through the universal
sx prop so it tracks the theme instead of hard‑coding values.
Color tokens (primary, surface, text-muted,
…), gradient refs (gradient.brand), spacing tokens
(md, lg), and radii all resolve to the same
--rui-* variables your theme defines:
$theme({ colors: { primary: "#0d9488" } })
$app(Row([
Card([Text("Token-styled", { sx: { color: "primary", weight: "700" } })],
{ sx: { bg: "surface", p: "lg", radius: "lg", shadow: "md" } }),
Card([Text("Gradient bg")],
{ sx: { bg: "gradient.brand", p: "lg", radius: "lg", color: "#fff" } })
], { gap: "md" }))
Because these are tokens, one $theme({…}) override reskins every
usage at once. See the Layout guide and the
universal props reference for the
full sx surface.
Layers, z-index & motion
Two specialised groups make app‑wide behaviour brandable. zIndex
tokens (base → 0, dropdown →
1000, modal → 1300, toast
→ 1400, tooltip → 1500, …) feed
the sx.zIndex prop, so a single override re‑orders your entire
layering stack. motion tokens (fast, base,
slow, ease) brand durations and easing curves:
$theme({
zIndex: { modal: 2000, toast: 2100 },
motion: { fast: "100ms", ease: "cubic-bezier(.2,.8,.2,1)" }
})
Per-response vs host theme
The two layers compose cleanly. The host sets a base with the
theme="…" attribute (one of the seven built‑ins, or a flat
JSON token map). A program then layers a per‑response overlay
by writing a bare $theme({…}) statement — it applies only
the tokens you pass, so everything else still inherits the host theme. The overlay
wins for the tokens it sets; delete the line and the UI snaps back to the base.
Because the overlay is scoped to a single <aktion-app> embed,
themes never leak across responses — the previous render's tokens are cleared
before the next render applies its own.
Dark mode
There is a first‑class built‑in dark theme — no
token authoring required. Set it on the host and your whole tree picks up the
dark surfaces, text, borders, and shadows:
<aktion-app theme="dark"></aktion-app>
Layer a per‑response $theme({…}) on top of dark
to brand it — e.g. override colors.primary while keeping the dark
surfaces underneath. To follow the OS preference, read it in the host page and call
app.setTheme(prefersDark ? "dark" : "light").
Right-to-left & direction
Theming and layout are direction‑aware. Set dir="rtl" (or
"auto") on <aktion-app> to flip the whole tree;
logical sx props like px/mx and
ps/pe mirror automatically. The direction key
inside $theme({…}) is advisory metadata only — use the
dir attribute to actually flip the layout.
<aktion-app theme="light" dir="rtl"></aktion-app>
Next
Layout
Column, Row, and Grid — the primitives every themed screen is built from.
Learn layout → ReferenceComponent catalog
Every component and prop, with live previews that respond to the active theme.
Browse components → Start hereGet started
Drop in <aktion-app>, render your first program, and pick a theme.