Get started in 30 seconds.
Three steps: load the bundle, mount the tag, and feed it Aktion text. Everything else — parsing, rendering, state, theming, routing, even Font Awesome — is handled inside the shadow DOM.
1. Load the bundle
Use the CDN build for zero setup:
<script type="module"
src="https://asfand-dev.github.io/aktion/dist/aktion.js">
</script>
Or install from npm if you bundle your own assets:
npm install aktion-runtime
// then, somewhere in your app entry:
import "aktion-runtime";
The bundle is a single ES module (~110 KB minified, ~30 KB gzipped). It registers exactly one custom element — <aktion-app> — and ships:
- The parser, evaluator, reactive runtime, and reconciler.
- All 130+ built-in components (layout, forms, charts, data, patterns).
- The hash-based router and seven built-in themes.
- An optional Font Awesome 6.7.2 loader (only injected if your script uses icons).
2. Mount the element
Drop the tag anywhere in your HTML:
<aktion-app theme="light"></aktion-app>
Optional attributes:
theme—light(default),dark,neon,pastel,glass,brutalist,skyline.response— static script text. Updates re-render immediately.streaming— boolean. Suppresses transient parse errors while a stream is in flight.showerrors— boolean. Enables the developer error banner inside the shadow DOM.
You can also drop the script text directly as the element’s text content — the runtime reads textContent on connect if no response attribute is set.
3. Send it Aktion
For static UI, set the response attribute (or the matching property):
<aktion-app></aktion-app>
<script>
const el = document.querySelector("aktion-app");
el.setResponse(`
_app_ = Stack([greeting])
greeting = Card([
CardHeader("Hello", subtitle: "Generative UI in plain HTML"),
Markdown("Set this script via **setResponse()** or stream it via **appendChunk()**.")
])
`);
</script>
For streaming responses (e.g. SSE from an LLM), set streaming = true while you append chunks. The UI commits each line as soon as the parser can — setting streaming just tells the error banner not to flash for transient mid-line parse errors.
const el = document.querySelector("aktion-app");
const reader = response.body.getReader();
const decoder = new TextDecoder();
el.clear();
el.streaming = true;
while (true) {
const { value, done } = await reader.read();
if (done) break;
el.appendChunk(decoder.decode(value, { stream: true }));
}
el.streaming = false;
Public API at a glance
The element is the entire integration surface:
// Attributes (set in HTML)
<aktion-app
theme="dark"
showerrors
response="...">
</aktion-app>
// Properties (set in JS)
el.response = "_app_ = ..." // same as setResponse(text)
el.streaming = true // toggle streaming mode
el.showErrors = true // toggle the developer error banner
// Methods
el.setResponse(text) // replace the entire script
el.appendChunk(chunk) // append a streaming chunk
el.clear() // wipe state and program text
el.setTheme(theme) // built-in name, or a Theme token map
el.registerComponents(specs) // extend the component library at runtime
el.setTools({ name: fn }) // expose async tools to js{} blocks
el.registerHttpInterceptors(...) // hook every http() call
el.navigate(path) // programmatic router navigation
el.getSystemPrompt() // build the LLM system prompt
el.applyDelta(ops) // Delta Protocol structural patches
el.serializeState() / hydrateState(snap) / loadSnapshot({...}) // persistence
// Events (CustomEvent on the element)
"assistant-message" // detail: { message } — Button without action:
"route-change" // detail: { path, previousPath, source }
"error" // detail: { kind, errors }
// + any user-emitted event from an action/effect via `emit`
Hello, world
Try it in a single file. Save the snippet below as index.html and open it — no build, no bundler.
<!doctype html>
<html lang="en">
<body>
<aktion-app theme="light"></aktion-app>
<script type="module">
import "https://asfand-dev.github.io/aktion/dist/aktion.js";
const el = document.querySelector("aktion-app");
el.setResponse(`
$count = 0
_app_ = Card([
CardHeader("Counter demo", subtitle: "Reactive state in 5 lines"),
Text("Count: " + $count),
Button("Increment", action: increment)
])
action increment() { $count = $count + 1 }
`);
</script>
</body>
</html>
Developer error banner
While iterating, enable the in-shadow error banner:
<aktion-app showerrors></aktion-app>
When enabled, parse and evaluation errors render as a compact banner
above your UI. The banner is automatically suppressed while
streaming = true so partial mid-line content doesn’t
flash transient errors. Listen for the error event to
forward diagnostics to your own logger or DevTools.
el.addEventListener("error", (event) => {
console.warn("Aktion:", event.detail);
});
System prompt
To make an LLM emit valid Aktion, prepend the bundled
system prompt to your conversation. Two flavors ship in dist/:
dist/system_prompt.txt— full reference (~30k tokens), best for capable models.dist/system_prompt_chat.txt— compact chat-mode prompt, optimized for smaller models.
const SYSTEM_PROMPT = await fetch("/dist/system_prompt.txt").then(r => r.text());
const messages = [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: "Show me a pricing page for a SaaS product." },
];
// Stream the response and forward chunks to the component.
streamLlmResponse(messages, (chunk) => el.appendChunk(chunk));
Both files are regenerated from src/prompt/generator.ts at build time, so they always match the components, themes, and builtins shipped in the same release.
Where to next?
Framework integration
Drop-in setup for React, Next.js, Vue, Nuxt, Angular, Svelte, Solid, Astro, Remix, and plain HTML.
See recipes → LanguageLanguage reference
Statements, expressions, reactive state, HTTP, actions, effects, control flow.
View reference → Try itPlayground
Live editor + preview. Tweak Aktion and watch the UI re-render on every keystroke.
Open playground →