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:
| Flag | What 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,
com1–com9, lpt1–lpt9).
The target must also be empty apart from a .git directory, so
scaffolding never overwrites work in progress.
Six templates ship today:
- empty — a minimal hello-world starter (no tests).
- dashboard — a home-automation control panel: a
$store, device switches/sliders, scenes, and energy charts behind anAppShell+ router. - website — a pet-sitting company site: navbar, hero, services, a pricing table + FAQ, and a validated contact form.
- todos-app — REST CRUD with
$http(create / toggle / edit / delete) andAsyncloading states. - chatbot — an OpenAI chat UI with an API-key settings popover and an offline echo fallback.
- portfolio — a developer portfolio with a filterable projects grid, an experience timeline, and a contact form.
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:
| File | What 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:
$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:
$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.
$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
Layout
Column, Row, Grid, and Center — every layout from three primitives.
Learn layout → ReferenceLanguage
State, expressions, actions, effects, HTTP, and the rules behind component calls.
View reference → ReferenceComponents
Every built-in component, its props, and a live preview you can copy from.
Browse components → FrameworksFrameworks
Drop-in setup for React, Vue, Angular, Svelte, Next.js, and plain HTML.
See recipes →