v0.6.5 A new language for building user interfaces

Build complex UIs in a fraction of the code.

Aktion is a reactive UI language — a strict subset of JavaScript you already know. It replaces your framework, component kit, router and i18n with one tag: 282 built-in components, fine-grained reactivity, 11 themes, and full JS interop — no JSX, no bundler, no boilerplate. Can also be effectively used as a Micro-Frontend in React, Vue, Angular, Svelte, Next.js or any other Framework.

Learn it in an afternoon · Runs in any framework · LLM-friendly by design
reply.aktion Live render
$app(Column([
  PageHeader("Project Atlas", { subtitle: "12 active · 4 at risk", badge: Badge("On track", { tone: "success" }) }),
  Grid([
    StatCard("Revenue", { value: "$48.2k", trend: "up", delta: "+12%", icon: "sack-dollar" }),
    StatCard("Users", { value: "12,540", trend: "up", delta: "+184", icon: "users" }),
    StatCard("Churn", { value: "1.4%", trend: "down", delta: "-0.3%", icon: "arrow-trend-down" })
  ], { gap: "md", minChildWidth: "150px" }),
  Card([
    CardHeader("Welcome back"),
    Text("This dashboard streamed in as plain text and rendered as a live UI.")
  ])
], { gap: "lg" }))

One custom element · drop it anywhere

React Vue Angular Svelte Plain HTML No framework
Less code, more shipping

The same UI, a fraction of the code.

No imports, no CSS files, no build config. Describe the interface as plain data and Aktion owns the DOM, the styling, and the updates.

Counter.jsx React · 3 files
import { useState } from "react"
import "./Counter.css"

export function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div className="card">
      <h3>Counter</h3>
      <p className="value">{count}</p>
      <button className="btn"
        onClick={() => setCount(c => c + 1)}>
        Increment
      </button>
    </div>
  )
}
/* + Counter.css + bundler + JSX build */
counter.aktion Aktion · 1 file
// no imports · no CSS · no build
$count = 0

$app(Card([
  CardHeader("Counter"),
  Text(`${$count}`),
  Button("Increment", { onClick: () => { $count = $count + 1 } })
]))
~60% less code 0 build config 1 file, not three styled & themeable
Why developers switch

One language. Everything a frontend needs.

Components, state, routing, theming, i18n and data — built in, batteries included. No glueing together five libraries, no build pipeline, no boilerplate.

Looks like JavaScript — because it is

Aktion is a strict subset of JS you already know: function, for…of, template literals, arrow functions, destructuring, optional chaining. Nothing new to memorize, no JSX.

subset of JSno JSXno build

One reactive sigil — $state

Declare state with $name = value, read it in your tree, write it in a function. The runtime tracks dependencies automatically at path granularity — no hooks rules, no useMemo dance.

$count = 0auto-trackedfine-grained

282 components

Layout, forms, charts, tables, calendars, gestures, drawing canvases, marketing sections, and full patterns like dashboards and app shells — included, not installed.

11 themes + design tokens

Switch palettes with one attribute, or brand the whole UI in-script with $theme({…}) over 113 tokens.

Built-in i18n

$i18n({ translations }) gives you t(), language switching and {name} interpolation — no extra library.

Router + global stores

$router, NavLink and params ship in the box, alongside $store for shared global state.

Use any JS library

The full JavaScript surface is in scope — fetch, Intl, crypto, JSON, Date, Math — plus a $util helper namespace. Bring your own logic.

One HTTP primitive

$http({ url }) returns a reactive resource with .data, .loading, .error & .refetch().

Less code, easier to maintain

A whole dashboard is a handful of plain-data calls. No flexbox bookkeeping, no component boilerplate, no global-CSS collisions — everything lives in an isolated Shadow DOM. Ship dramatically fewer lines and read them again next quarter.

shadow-DOM scopeddeclarativemaintainable

LLM-friendly by design

The same simplicity that makes Aktion fast for you makes it trivial for an AI to write. A built-in system-prompt generator and streaming renderer turn any model into a UI engine — but you never need an LLM to use Aktion.

getSystemPrompt()streamingoptional
AI-native, not AI-only

And yes — an LLM can write it too.

The same plain-data simplicity that speeds you up makes Aktion trivial for a model to generate. It streams token-by-token straight into the DOM — a free superpower, never a requirement.

1 · LLM streams

The model emits Aktion text token by token.

2 · Parser commits

Each line is parsed and applied as it lands.

3 · UI renders

A React-like reconciler diffs & paints live.

Rendering as it streams
Generative UI in chat

Your assistant replies with a real interface.

Instead of a wall of text, the model streams Aktion straight into the conversation — and it renders token-by-token as a live, interactive UI. Every card below is a real <aktion-app>, not a screenshot. Try the arrows.

Sprint Dashboard Live
Fine-grained reactivity

Path-level tracking. No wasted renders.

Reading $user.name subscribes to that path alone — a write to $user.role never re-renders, recomputes, or re-fires an effect that only read name.

  • Automatic dependency trackingThe $ sigil is the only thing that makes a binding reactive.
  • Two-way binding for freeBind a value with value: $form.email and it just works.
  • Declarative side effects$effect(fn, [$dep, "mount", "debounce(300)"]) with lifecycle & rate-limit triggers.
  • State survives re-rendersInput focus, selection, scroll, and IME state are preserved by the reconciler.
counter.aktion Try it
$count = 0
$step = 1
function inc() { $count = $count + $step }
function dec() { $count = $count - $step }
function reset() { $count = 0 }
$app(Card([
  CardHeader("Reactive counter", { subtitle: "State is the one piece of magic" }),
  Center([Text(`${$count}`, { variant: "display-heavy" })]),
  Row([
    Button("−", { variant: "secondary", onClick: dec }),
    Button("Reset", { variant: "ghost", onClick: reset }),
    Button("+", { variant: "primary", onClick: inc })
  ], { gap: "sm", justify: "center" })
], { gap: "md" }))
How it compares

A real alternative to React, Vue & Angular.

Everything you normally bolt together — a framework, a component kit, a router, an i18n library, a styling system — built into one tag, with far less ceremony.

Capability Aktion React Vue Angular
No build step / no JSX One script tag Bundler Bundler CLI + build
Code to ship the same UI ~60% less Boilerplate Moderate Verbose
Built-in i18n & router Included Libraries Libraries Built-in
Components included 270+ built-in Bring your own Bring your own Material only
Framework-agnostic Any / none React only Vue only Angular only
Style isolation by default Shadow DOM CSS-in-JS Scoped ViewEncaps.
Built-in themes & tokens 7 + custom Library Library Material
Component library

282 components. All live.

Every preview below is a real <aktion-app> — not a screenshot. Compose data, not DOM.

Charts & stats
$app(Column([kpi, chart], { gap: "md" }))
kpi = StatCard("Active users", { value: "8,420", trend: "up", delta: "+12%", icon: "users" })
chart = BarChart(labels, [Series("Users", values)], { title: "Daily Activity" })
labels = ["Mon", "Tue", "Wed", "Thu", "Fri"]
values = [32, 51, 44, 68, 59]
Forms & inputs
$app(settingsCard)

$plan = "pro"
$notify = true
$volume = 60

function save() {
  $toast.success("Settings saved")
}

settingsCard = Card([
  CardHeader("Settings"),
  Column([
    FormControl("Plan", Select("plan-select", {
      items: planOptions,
      value: $plan
    })),
    FormControl("Notifications", Switch("notify-switch", {
      label: "Email notifications",
      value: $notify
    })),
    FormControl("Volume", Slider("volume-slider", {
      value: $volume,
      min: 0,
      max: 100,
      showValue: true
    })),
    Button("Save changes", {
      onClick: save,
      variant: "primary",
      icon: "check",
      fullWidth: true
    })
  ], { gap: "md" })
])

planOptions = [
  SelectItem("free", "Free"),
  SelectItem("pro", "Pro"),
  SelectItem("team", "Team")
]
Data & feedback
$app(Column([callout, userTable, statusBadges], { gap: "md" }))
callout = Callout("Heads up", { tone: "info", description: "Your trial ends in 3 days." })
userTable = Table([
  Col("Name", ["Ada Lovelace", "Alan Turing", "Grace Hopper"]),
  Col("Role", ["Owner", "Admin", "Member"]),
  Col("Status", [
    Badge("Active", { tone: "success" }),
    Badge("Active", { tone: "success" }),
    Badge("Invited", { tone: "warning" })
  ])
])
statusBadges = Row([
  Badge("Live", { tone: "success" }),
  Badge("Beta", { tone: "warning" })
], { gap: "sm" })
Theming

Eleven themes. Infinite brands.

Three re-create shadcn/ui, Material UI and HeroUI, light and dark; Signal is our own instrument console for dashboards and ops. Switch palettes with the theme attribute, or override 80+ design tokens in-script with $theme({…}).

Light
Dark
shadcn
shadcn‑dark
MUI
MUI dark
HeroUI
HeroUI dark
Signal
Signal dark
Soft
Your brand →
For developers

Three steps to your first UI.

Load the bundle, mount the tag, and write Aktion — a JavaScript subset you already know. No CLI, no config, no JSX. It runs the same inside React, Vue, Angular or a plain HTML file.

  • LoadOne ESM script tag from the CDN — or npm i aktion-runtime.
  • MountAdd <aktion-app theme="dark"> wherever HTML runs.
  • BuildWrite state, functions and components, then call $app(…). That's the whole app.
index.html — the whole app
// 1 · one tag, any framework
<script type="module" src="https://asfand-dev.github.io/aktion/dist/aktion.js">
<aktion-app theme="dark">

// 2 · write Aktion — a JS subset
  $greeting = "Hello"
  $app(Card([
    CardHeader($greeting),
    Button("Wave", { onClick: () => { $greeting = "Hi!" } })
  ]))

</aktion-app>
no build · no JSX · runs anywhere
Orchestration

One language. Any design system.

Aktion acts as an orchestration layer. Write your UI logic once, and let Aktion map it to Material UI, Bootstrap, ShadCN, or your own in-house framework.

card.aktion — one quine, three outputs
// Aktion quine (single source)
$app(Card([
  CardHeader("Revenue dashboard"),
  Text("Mapped through a UI provider."),
  Row([
    Button("Primary", { variant: "primary" }),
    Button("Secondary", { variant: "ghost" })
  ], { gap: "sm" })
]))

// Provider bundles are loaded via CDN, not npm install.
Material UI
@aktion/material-ui-provider
<Card>
  <CardHeader title="Revenue dashboard" />
  <CardContent><Typography>Mapped through a UI provider.</Typography></CardContent>
  <CardActions><Button variant="contained">Primary</Button></CardActions>
</Card>
Bootstrap
@aktion/bootstrap-provider
<div class="card">
  <div class="card-header">Revenue dashboard</div>
  <div class="card-body"><button class="btn btn-primary btn-sm">Primary</button></div>
</div>
ShadCN
@aktion/shadcn-provider
<Card>
  <CardHeader><CardTitle>Revenue dashboard</CardTitle></CardHeader>
  <CardFooter><Button>Primary</Button></CardFooter>
</Card>

A faster way to build UIs.

Learn it in an afternoon. Ship complex, themeable, reactive interfaces in any framework — with a fraction of the code. Open source under MIT.

<script src="https://asfand-dev.github.io/aktion/dist/aktion.js"> + <aktion-app>