Integration

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>.

Live
$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:

  1. Register the element by importing the bundle once in client‑side code — import "aktion-runtime"; defines the <aktion-app> custom element on the page.
  2. Give it a program. Either set the response attribute (response="$app(…)") or call setResponse(text) on the element. To stream tokens from an LLM, set the streaming attribute and call appendChunk(text) as chunks arrive, then drop streaming when the stream ends.
  3. Listen for events. The element emits standard bubbling, composed CustomEvents: assistant-message (a follow‑up message, on event.detail.message), route-change (hash route changed, on event.detail), and error (parse failures, on event.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(&quot;Hi&quot;))"></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