Getting started

Install.

Get Aktion rendering in under a minute. The quickest path is to scaffold a project with create-aktion and add the editor extension — then you write a compact $app(Column([…])) program and the <aktion-app> element turns it into a real, interactive UI. Prefer no build step? You can also drop in a CDN tag or install from npm.

Scaffold a project with create-aktion

The fastest way to a real, hot-reloading app is create-aktion (requires Node 18+). It generates a Vite + TypeScript project with multi-file .aktion modules, the aktion-runtime/vite plugin wired up, and — for the richer templates — a Vitest suite:

npm create aktion@latest my-app
cd my-app
npm install
npm run dev          # http://localhost:5173

Run it with no arguments to choose a name and template interactively, or pass --template to pick one up front (pnpm create aktion, yarn create aktion, and npx create-aktion work too):

npm create aktion@latest my-app -- --template dashboard

The prompts only appear in a TTY. In CI, pass -y so it takes the defaults instead of waiting for input:

FlagWhat it does
-t, --template <name> Template to scaffold. Default empty. --template=<name> also works, and vite-ts is kept as an alias for empty.
--pm <npm | pnpm | yarn | bun> Which package manager the printed next‑steps commands use. It changes the instructions, not the scaffold. Detected from npm_config_user_agent when omitted.
-y, --yes Skip every prompt and use the defaults. Required in CI and any non‑TTY shell.
-h, --help Print the usage block, including the template list.

The project name becomes a directory, so it is validated rather than normalised. A plain name (my-app) or a nested one (apps/my-app) is fine, but a name that would write outside the directory you are standing in is refused rather than cleaned up.

Rejected: control characters, a leading - (a downstream tool would read it as a flag), a leading ~, an absolute path or Windows drive letter, any .. segment, and the Windows reserved device names (con, prn, aux, nul, com1com9, lpt1lpt9). The target must also be empty apart from a .git directory, so scaffolding never overwrites work in progress.

Six templates ship today:

Every template but empty splits its UI across .aktion files the way you would in React (data / store / components / pages) and ships unit tests you run with npm test. Editing any .aktion file hot-reloads the UI while preserving live $state.

Every template is built on the same four files, whichever one you pick:

FileWhat it does
vite.config.ts import aktion from "aktion-runtime/vite" plus plugins: [aktion()]. The plugin links your .aktion graph at build time and drives HMR — see its options.
index.html <aktion-app id="app" theme="light"> and a module script pointing at src/main.ts. The <title> is stamped with your project name.
src/main.ts Imports aktion-runtime (which registers the element), imports ./app.aktion as a compiled program, and calls mountCompiled() on the element.
src/app.aktion The entry module — it owns the rendered root, normally a $app(…) statement. Richer templates import the rest of the UI from sibling files.

Also generated: a tsconfig.json (ES2022, moduleResolution: "Bundler", strict), src/env.d.ts so TypeScript knows what a .aktion import is, a .gitignore, and the .vscode/ folder described below.

Editor support (VS Code & Cursor)

Install the Aktion extension for first-class .aktion editing — syntax + semantic highlighting, inline diagnostics, completions, hover docs, signature help, go-to-definition (including across imported files), find references, rename, an outline view, and format-on-save. Open the Extensions view, search “Aktion”, and click Install — or from a terminal:

code --install-extension AsfandiyarKhan.aktion-vscode

It is available on the Visual Studio Marketplace (VS Code) and on Open VSX (Cursor, VSCodium, Gitpod, …). Projects scaffolded with create-aktion already recommend it and enable format-on-save and semantic highlighting for .aktion files, so new contributors get the prompt to install it automatically.

Your first program

Here is a complete program. The live preview on the right is the same bundle you just installed, rendering this exact source:

Live
$app(Column([
  PageHeader("Project Atlas", { subtitle: "Updated 2 hours ago" }),
  Card([
    CardHeader("Overview"),
    Text("Every Aktion program is a tree of components.")
  ]),
  Button("Get started", { variant: "primary", icon: "rocket" })
], { gap: "lg" }))

Every program must call $app(…) — that expression is the root the element renders. The root is normally a Column([…]), the recommended layout primitive that stacks children top to bottom; its siblings are Row, Center, and Grid. Everything else is just components nested inside, each a function call with an optional props object.

Add reactivity

State lives in $-prefixed bindings. Reassign one and every part of the UI that reads it re-renders — no hooks, no setState. Wire a Button to a function with onClick:

Live
$count = 0
function inc() { $count = $count + 1 }
$app(Card([
  CardHeader("Counter", { subtitle: "Reactive state in a few lines" }),
  Text("Count: " + $count),
  Button("+1", { onClick: inc, variant: "primary" })
]))

Action bodies are real JavaScript: by default the full global surface is available (alert, fetch, Math, …), as are timers like setTimeout and setInterval. For data, reach for the built-in $http(…) resource — it tracks loading and error state and exposes an .onDone callback for when a request settles.

How much of this is trusted?

An Aktion program is code, not data. Because unshadowed identifiers fall through to the host page’s globals, a program is as privileged as a <script> tag you wrote yourself — so author one exactly as carefully. That is the right default when the program lives in your repo.

When it does not — generated by an LLM, stored per tenant, edited by users — narrow the surface with setGlobalAccessPolicy("safe") before mounting anything. The library’s sanitisers make untrusted data safe to render through a trusted program; they do not contain an untrusted program. Security covers both halves.

Theming in one line

Switch the entire look with a single attribute — <aktion-app theme="dark">. Built-in themes include light, dark, soft, and a light and dark variant each of shadcn, mui, heroui and signal; you can also customize the theme with setting the theme variables in $theme({...}). See Themes for the full list and custom theme variables.

Live — this demo follows the page theme
$app(Card([
  CardHeader("Themed surface", { subtitle: "One attribute restyles everything" }),
  Text("Try toggling the doc theme in the top bar."),
  Button("Action", { variant: "primary" })
]))

$theme({ colors: { primary: "orange" } })

Driving the element from JavaScript

For static UI the response attribute is enough. To swap the program at runtime, grab the element and call setResponse() (the response property does the same thing):

const app = document.querySelector("aktion-app");

app.setResponse(`
  $app(Column([
    CardHeader("Loaded from JavaScript"),
    Text("Call setResponse() to replace the whole program.")
  ]))
`);

For a streaming source (e.g. an LLM responding token by token), turn on the streaming attribute and push text with appendChunk(). The UI commits each line as soon as the parser can; streaming simply tells the error banner not to flash on transient mid-line parse errors. Clear it when the stream ends.

const app = document.querySelector("aktion-app");
app.setAttribute("streaming", "true");

for await (const chunk of stream) {
  app.appendChunk(chunk);
}

app.removeAttribute("streaming");

To keep your program in its own file instead of inline markup, point the src attribute at an .aktion file. It is resolved relative to the page and linked through the in-browser project linker, so an entry that imports other modules fetches its whole graph on connect:

<aktion-app src="./app.aktion"></aktion-app>

Precedence is response (attribute or setResponse()), then src, then inner text. Fetch or link failures surface through the same error banner and error event as parse errors.

Three more attributes are worth knowing up front. theme selects a built-in theme (see Theming above); showerrors renders a developer error banner inside the shadow DOM while you iterate; and strict turns on extra development diagnostics — most usefully a warning when a commit undoes an attribute an event handler wrote straight onto the live DOM.

Other methods on the element include setTheme(), registerComponents() to extend the library at runtime, and getSystemPrompt() to build the prompt that teaches an LLM to emit Aktion. Frameworks has the full attribute and event reference.

If your program is already compiled — because a bundler ran the .aktion linker, or because you called compileLite() / linkProject() yourself — mount it with mountCompiled(program) instead of setResponse(). That path skips the parser and takes an optional state snapshot as a second argument, which is how hydration and hot reload preserve live $state. See Modules → Linking from JavaScript.

Other ways to install Aktion

create-aktion is the recommended start, but you can also add Aktion to a page or an existing app with no scaffolding.

Drop in a script tag (CDN)

Zero build, zero bundler. Load the ES module bundle from the CDN — it registers the <aktion-app> element — then mount the tag and hand it a program through the response attribute:

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

<aktion-app response="$app(Column([
  PageHeader('Hello', { subtitle: 'Generative UI in plain HTML' }),
  Card([CardHeader('It works'), Text('No build step required.')])
]))"></aktion-app>

That single bundle ships the parser, the reactive runtime, every built-in component, the themes, and an optional Font Awesome loader. Nothing else to wire up.

Install aktion-runtime from npm

Adding Aktion to an existing app (React, Vue, Svelte, plain HTML…) or bundling it yourself? Install the runtime package directly:

npm i aktion-runtime

Then import it once, anywhere in your app entry. The import has a side effect: it registers the <aktion-app> custom element. After that the tag works in any framework or in plain HTML.

import "aktion-runtime"; // registers <aktion-app>

// then render the element however your framework does it:
// <aktion-app response="$app(Column([ Text('Hi from npm') ]))"></aktion-app>

Next