v0.5.15 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: 270+ built-in components, fine-grained reactivity, 6 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

270+ components

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

6 themes + design tokens

Switch palettes with one attribute, or brand the whole UI in-script with $theme({…}) over 80+ 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

270+ 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

Six themes. Infinite brands.

Switch palettes with the theme attribute, or override 80+ design tokens in-script with $theme({…}).

Light
Dark
Corporate
Soft
Glass
Modern
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

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>