Aktion · brand themes ← Back to demos
Live demo · $theme({...})

Same UI, six brands.

Pick a brand below — the preview reskins to a GitHub, Apple, Stripe, IONOS, Notion or Vercel-flavoured palette using only a bare $theme({...}) line at the top of the program. No CSS, no host configuration, no custom components. Copy any block into your own response to brand a single reply, or feed the flat token map through el.setTheme(...) to brand the whole host.

Each palette is an approximation assembled from publicly visible colours and type stacks, kept deliberately small so you can see which tokens carry a brand. They are illustrations of the token system, not reproductions of anyone's design system.

Pick a brand

Each chip swaps just the $theme({...}) block at the top of the program. The rest of the Aktion program is identical across every brand — the renderer picks up the new tokens between renders, without re-mounting a single component.

Flat token maps, grouped output

Each preset on this page is stored as a flat ThemeTokens map — colorPrimary, radiusButton, buttonFontWeight — because that is the shape el.setTheme(...) and the theme attribute take. The Theme source panel above shows the same palette translated into the grouped $theme({...}) shape a program uses, and that translated block is what the live preview actually runs.

The two shapes are not quite interchangeable. Ten groups exist — colors, radius, font, spacing, shadows, gradients, zIndex, motion, plus the two side-effect groups fonts and icons — but 25 tokens have no group at all. The button* padding and weight, the six chart* series colours, borderWidth, letterSpacingHeading and lineHeight* only reach the host through the flat path, so the grouped block you copy from here omits them. Every preset on this page uses some of them, which is exactly why the flat map is the source of truth.

One more gap worth knowing about: these presets retint the status fills (colorSuccess and friends) but not their text-safe partners (colorSuccessText, colorOnSuccess, …) or colorBorderControl. Those inherit the light theme's accessible values, so status labels stay on the built-in hue rather than the brand's. Add the partners to finish a brand — see status colours come in threes.

The shared program

Every brand renders the same Aktion source. Only the $theme({...}) line at the top changes — the runtime applies the new tokens to the host as CSS custom properties, so the existing components instantly pick up the new palette, typography, and shape.

head = PageHeader(
  "Acme Platform",
  {
    subtitle: "All your operations, one workspace.",
    breadcrumbs: ["Workspace", "Dashboard"],
    actions: [
      Button("Settings",    { variant: "secondary", size: "sm", icon: "gear" }),
      Button("New project", { variant: "primary",   size: "sm", icon: "plus" })
    ],
    badge: Badge("On track", { tone: "success" })
  }
)

kpis = Stats([
  StatCard("Active deals",    { value: "1,284",  trend: "up",   delta: "+12%", icon: "briefcase" }),
  StatCard("Monthly revenue", { value: "$48.2k", trend: "up",   delta: "+8%",  icon: "sack-dollar" }),
  StatCard("Open tickets",    { value: "42",     trend: "down", delta: "-15%", icon: "ticket" }),
  StatCard("Churn",           { value: "1.4%",   trend: "flat", delta: "0",    icon: "user-minus" })
])

trendChart = Card([
  SectionHeader("Revenue trend", {
    subtitle: "Last 12 weeks",
    actions: [Button("Export", { variant: "ghost", size: "sm", icon: "arrow-up-from-bracket" })]
  }),
  LineChart({
    labels: ["W1","W2","W3","W4","W5","W6","W7","W8","W9","W10","W11","W12"],
    series: [
      Series("This quarter", { values: [22, 28, 26, 32, 34, 31, 38, 42, 44, 41, 46, 48] }),
      Series("Last quarter", { values: [18, 21, 20, 24, 27, 25, 28, 31, 33, 30, 34, 36] })
    ]
  })
])

teamCard = Card([
  SectionHeader("Team", { subtitle: "6 members", eyebrow: "Engineering" }),
  Stack([
    PersonChip("Asha Patel",   { role: "Engineering lead", size: "md", status: "online" }),
    PersonChip("Linus Wong",   { role: "Senior developer", size: "md", status: "online" }),
    PersonChip("Mira Sanchez", { role: "Product designer", size: "md", status: "away" }),
    PersonChip("Sam Reyes",    { role: "QA engineer",      size: "md", status: "offline" })
  ], { direction: "column", gap: "sm" })
])

activityCard = Card([
  SectionHeader("Recent activity", { subtitle: "Last 24h" }),
  Timeline([
    TimelineItem("Deployed v2.4.1",          { time: "12:04", icon: "rocket",               tone: "primary" }),
    TimelineItem("Closed 4 support tickets", { time: "11:18", icon: "circle-check",         tone: "success" }),
    TimelineItem("Updated billing API",      { time: "10:02", icon: "credit-card",          tone: "info" }),
    TimelineItem("Investigating latency",    { time: "08:47", icon: "triangle-exclamation", tone: "warning" })
  ])
])

planCard = Card([
  SectionHeader("Workspace plan", { subtitle: "Renews in 12 days" }),
  ProgressRing(72, { max: 100, label: "12 / 17", description: "Seats used", tone: "primary" }),
  Buttons([
    Button("Manage seats", { variant: "secondary", size: "sm" }),
    Button("Upgrade",      { variant: "primary",   size: "sm" })
  ])
])

quickForm = Card([
  SectionHeader("Quick capture", { subtitle: "Add a new task" }),
  FormControl("Title", { control: Input("title", { placeholder: "Ship the analytics tab" }) }),
  FormControl("Owner", { control: Select("owner", { items: [
    SelectItem("asha",  "Asha Patel"),
    SelectItem("linus", "Linus Wong"),
    SelectItem("mira",  "Mira Sanchez")
  ], value: "asha" }) }),
  Buttons([
    Button("Add task", { variant: "primary" }),
    Button("Cancel",   { variant: "ghost" })
  ])
])

bottomGrid = Grid([teamCard, activityCard, planCard, quickForm], { columns: 2, gap: "lg" })

$app(Stack([head, kpis, trendChart, bottomGrid], { direction: "column", gap: "lg" }))

Use it from your app

Three ways to apply a brand, in three different languages. Pick by where the decision lives: in the model's response, in your host JavaScript, or in your page's stylesheet.

1 · Aktion — brand one response (grouped shape)
$theme({
  colors: { primary: "#0969da", primaryText: "#ffffff" },
  font:   { family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" },
  radius: { button: "6px" }
})
$app(Stack([CardHeader("Welcome"), Buttons([Button("Sign in")])]))

Inside a program the keys are grouped. Button weight is not available here — buttonFontWeight has no group, so it has to come from the host.

2 · JavaScript — brand the whole host (flat shape)
// The host paths take flat token names, and they reach every token —
// including buttonFontWeight, chart1..6 and borderWidth.
el.setTheme({
  colorPrimary:     "#0969da",
  colorPrimaryText: "#ffffff",
  radiusButton:     "6px",
  buttonFontWeight: "500",
  fontFamily:       "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
});

// Equivalently, hand the element a JSON token map on the attribute:
el.setAttribute("theme", JSON.stringify({ colorPrimary: "#0969da" }));

setTheme() replaces the whole base layer and resets the per-response overlay tracker; the next render reapplies any $theme({...}) from the program on top.

3 · CSS — set the variables directly
aktion-app {
  /* !important is required: see the note below. */
  --rui-color-primary: #0969da !important;
  --rui-radius-button: 6px !important;
  --rui-font-family:   -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
}

The --rui-* prefix is the public contract, so a stylesheet can drive it — with one catch. Applying a theme writes every token the theme defines as an inline style on the host element, and an inline declaration beats a normal stylesheet rule, so the !important is what makes this win. Tokens the active theme leaves unset (the optional ones, such as --rui-color-link or --rui-motion-fast) are not written inline, so those need no !important. Prefer options 1 and 2 unless the values genuinely have to come from CSS.