Frameworks.
<aktion-app> is a
framework‑agnostic
web component
— drop it into any app, feed it a program string, and listen to
its events. There is no framework‑specific build of Aktion:
it is one custom element that renders the same way inside React,
Vue, Svelte, Angular, or a bare HTML page.
What it renders
Whatever framework you wire it into, the element does one thing: it
takes a program string and renders the live UI inside its shadow DOM.
Here is a complete program and its output — this is exactly what
appears wherever you place <aktion-app>.
$app(Column([
Card([
CardHeader("Welcome to Aktion", { subtitle: "One web component, every framework" }),
Text("Feed this element a program string and it renders the UI for you."),
Row([
Badge("Framework-agnostic", { tone: "primary" }),
Spacer(),
Button("Get started", { variant: "primary", icon: "arrow-right" })
], { gap: "sm" })
])
], { gap: "lg" }))
The contract
Every integration comes down to the same three steps, regardless of framework:
-
Register the element by importing the bundle
once in client‑side code —
import "aktion-runtime";defines the<aktion-app>custom element on the page. -
Give it a program. Either set the
responseattribute (response="$app(…)") or callsetResponse(text)on the element. To stream tokens from an LLM, set thestreamingattribute and callappendChunk(text)as chunks arrive, then dropstreamingwhen the stream ends. -
Listen for events. The element emits standard
bubbling, composed
CustomEvents:assistant-message(a follow‑up message, onevent.detail.message),route-change(hash route changed, onevent.detail), anderror(parse failures, onevent.detail.errors).
Other useful methods on the element: setTheme(name | tokens),
registerComponents([…]) to extend the library,
registerHttpInterceptors({ onRequest, onResponse, onError })
for host‑wide auth/logging (calls merge incrementally), and
getSystemPrompt() to build the LLM system
prompt for the active component set. The observed attributes are
response, theme, streaming,
showerrors, dir
("ltr" / "rtl" / "auto" —
flips the whole tree for RTL), and scroll-restoration
("auto" / "top" — restore scroll across
in‑app navigation).
Server‑side rendering. For SSR/SSG, import
renderToString(program, opts) from aktion-runtime
to get { html, state } on the server, embed the markup in your
page shell, and serialise state for client hydration. The
renderer needs a DOM, so in Node register happy-dom /
jsdom on globalThis first. See
Performance → SSR.
Installing the bundle
The package is published to npm as aktion-runtime; the same bundle is served from the docs CDN for no‑build pages:
# npm (works with any bundler)
npm install aktion-runtime
# Then, once in your client-side entry point:
import "aktion-runtime";
# Or load from the CDN with no build step:
# <script type="module" src="https://asfand-dev.github.io/aktion/dist/aktion.js"></script>
Framework integrations
Pick your framework below for a copy‑paste integration. Every
snippet does the same three things from the contract above: register the
bundle once, hand <aktion-app> a program, and listen
for its events.
React
In JSX you write the custom element tag directly:
<aktion-app />. Import the bundle once (anywhere in
client code) so the element is registered, then use a
ref plus a useEffect to push the program in
with setResponse and to subscribe to events.
// AktionView.tsx
import { useEffect, useRef } from "react";
import "aktion-runtime"; // registers <aktion-app> once
type AktionEl = HTMLElement & {
setResponse(text: string): void;
};
export function AktionView({ program }: { program: string }) {
const ref = useRef<AktionEl | null>(null);
// Push the program in whenever it changes.
useEffect(() => {
ref.current?.setResponse(program);
}, [program]);
// Listen for the element's CustomEvents.
useEffect(() => {
const el = ref.current;
if (!el) return;
const onMessage = (e: Event) => {
console.log("follow-up:", (e as CustomEvent).detail.message);
};
el.addEventListener("assistant-message", onMessage);
return () => el.removeEventListener("assistant-message", onMessage);
}, []);
return <aktion-app ref={ref} theme="light" />;
}
Add the JSX intrinsic type once so TypeScript accepts the tag:
// aktion.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from "react";
declare global {
namespace JSX {
interface IntrinsicElements {
"aktion-app": DetailedHTMLProps<
HTMLAttributes<HTMLElement> & { theme?: string; response?: string; streaming?: boolean },
HTMLElement
>;
}
}
}
Vue 3
Tell the Vue compiler that aktion-app is a custom element
so it stops trying to resolve it as a Vue component — set
app.config.compilerOptions.isCustomElement (or the
equivalent option in your build tool's Vue plugin). Then bind the
program with :response, or grab a ref and call
setResponse.
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import "aktion-runtime"; // registers <aktion-app> once
const app = createApp(App);
// Treat any <aktion-app> tag as a native custom element.
app.config.compilerOptions.isCustomElement = (tag) => tag === "aktion-app";
app.mount("#app");
<!-- App.vue -->
<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
const program = ref(`$app(Card([CardHeader("Hello from Vue")]))`);
// Simplest case: bind the attribute directly.
// <aktion-app :response="program" theme="light" />
// Or use a ref for the imperative API + events:
const el = ref<(HTMLElement & { setResponse(t: string): void }) | null>(null);
onMounted(() => {
el.value?.setResponse(program.value);
el.value?.addEventListener("route-change", (e) =>
console.log((e as CustomEvent).detail),
);
});
watch(program, (next) => el.value?.setResponse(next));
</script>
<template>
<aktion-app ref="el" theme="light" />
</template>
Svelte
Svelte supports custom elements natively, so there is no configuration
step — just import the bundle and use the tag. Pass the program
with the response attribute and subscribe to events with
Svelte's on: directive (it maps straight to
addEventListener).
<!-- AktionView.svelte -->
<script lang="ts">
import "aktion-runtime"; // registers <aktion-app> once
export let program: string = `$app(Card([CardHeader("Hello from Svelte")]))`;
function onAssistantMessage(e: CustomEvent) {
console.log("follow-up:", e.detail.message);
}
</script>
<aktion-app
response={program}
theme="light"
on:assistant-message={onAssistantMessage}
on:route-change={(e) => console.log(e.detail)}
/>
For server‑rendered routes (SvelteKit), guard the import so it
only runs in the browser, e.g.
if (browser) await import("aktion-runtime");.
Angular
Add CUSTOM_ELEMENTS_SCHEMA to the component (or module) so
Angular's template compiler accepts the unknown tag, import the bundle
once, and bind the program with the [attr.response]
attribute binding.
// main.ts
import "aktion-runtime"; // registers <aktion-app> once
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent);
// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
@Component({
standalone: true,
selector: "app-root",
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<aktion-app
[attr.response]="program"
[attr.theme]="'light'"
(assistant-message)="onMessage($event)">
</aktion-app>
`,
})
export class AppComponent {
program = `$app(Card([CardHeader("Hello, Angular")]))`;
onMessage(event: Event) {
console.log("follow-up:", (event as CustomEvent).detail.message);
}
}
Vanilla HTML
No build step required. A single module‑type script tag imports
the bundle (registering the element); then place the
<aktion-app> tag and call setResponse.
<!doctype html>
<html>
<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(`$app(Card([CardHeader("Hello, world")]))`);
el.addEventListener("assistant-message", (e) => {
console.log("follow-up:", e.detail.message);
});
</script>
</body>
</html>
You can also set the program declaratively with the
response attribute —
<aktion-app response="$app(Text("Hi"))"></aktion-app>
— though the imperative setResponse is easier for
anything longer than a line.
For anything larger, keep the program in its own file and load it with the
src attribute —
<aktion-app src="./app.aktion"></aktion-app>. The file 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.
Next
Get started
Install the bundle, render your first program, and wire up streaming end to end.
Get started → ReferenceLanguage
State, expressions, control flow, and the rules behind component calls.
View reference → ReferenceComponent catalog
Every component, prop, and a live preview — the building blocks for your programs.
Browse components →