Theming

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:

LayerWhere it livesToken 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>
ThemeFeel
lightDefault. Crisp white surfaces, indigo accent, 1px borders.
darkHigh‑contrast dark surfaces, soft indigo accent.
corporateCloud‑console — deep navy + cyan, dense layout, small radii.
softSoft & rounded — lavender + mint, large radii, gentle shadows.
glassLight glassmorphism — frosted white surfaces with backdrop blur over an airy pastel gradient.
modernModern 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.

Live
$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:

Live
$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:

Live
$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:

GroupCSS variableKeys
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
fontsimport: ["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:

KeyCSS variableDefault (light)Role
bg--rui-color-bg#ffffffPage background.
bgSubtle--rui-color-bg-subtle#f8fafcSubtle page wash.
surface--rui-color-surface#ffffffCards, sheets, panels.
surfaceMuted--rui-color-surface-muted#f1f5f9Inset / secondary surfaces.
border--rui-color-border#e2e8f0Default 1px borders.
borderSubtle--rui-color-border-subtlergba(15,23,42,.08)Hairline dividers.
text--rui-color-text#0f172aPrimary body text.
textMuted--rui-color-text-muted#475569Secondary / caption text.
primary--rui-color-primary#6366f1Brand colour — primary buttons, active states.
primaryHover--rui-color-primary-hover#4f46e5Hover/pressed brand colour.
primaryText--rui-color-primary-text#ffffffText/icon on a primary fill.
accent--rui-color-accent#6366f1Secondary accent — links, chips, callouts.
accentHover--rui-color-accent-hover#4f46e5Hover accent.
accentText--rui-color-accent-text#ffffffText on an accent fill.
focusRing--rui-color-focus-ring#6366f1Keyboard focus outline.
success--rui-color-success#10b981Positive status.
warning--rui-color-warning#f59e0bCaution status.
danger--rui-color-danger#ef4444Error / destructive status.
info--rui-color-info#06b6d4Informational 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.

GroupKey → default
radiusxs 4px · sm 6px · md 10px · lg 16px · pill 999px · button 6px · input 6px
spacingxs 4px · sm 8px · md 12px · lg 20px · xl 32px · 2xl 48px · 3xl 80px
font sizessizeBase 14px · sizeSm 12px · sizeLg 16px · sizeHeading 16px · sizeTitle 22px
font weightsweightBody 400 · weightHeading 700
font familiesfamily / familyHeading system‑ui stack · familyMono ui‑monospace stack
shadowssm 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.

Live
$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:

Live
$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 (base0, dropdown1000, modal1300, toast1400, tooltip1500, …) 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