Advanced

JavaScript interactions.

Aktion is a strict subset of JavaScript, and inside function, effect, and lambda bodies you have the full JavaScript language and the entire browser global surface — dialogs, fetch, crypto, Intl, timers, window, and document. No imports, no wrappers: every global resolves by name.

Where you can use it

Aktion programs are declarative at the top level — you assign reactive state with $name and build the tree by composing components. Imperative JavaScript lives in three places, and in each of them the whole language is fair game:

State is read and written with the bare $ sigil — no bridge object, no getter/setter calls. Writing a $atom triggers a re-render:

$copied = false

function copyShareLink() {
  await navigator.clipboard.writeText(window.location.href)
  $copied = true
}

$effect(() => {
  const onScroll = () => { $scrollY = window.scrollY }
  window.addEventListener("scroll", onScroll, { passive: true })
  cleanup(() => window.removeEventListener("scroll", onScroll))
}, ["mount"])

Bodies run as async functions, so you can await anywhere inside a function or effect.

The full global surface

Beyond the curated standard library, every JavaScript global resolves by name as the final fallback in identifier resolution. You never import anything — just use the name.

GroupGlobalsNotes
Dialogs alert, confirm, prompt Synchronous and blocking — fine for quick confirmations; prefer a Modal for rich UX.
Network fetch, URL, URLSearchParams, Blob, FormData For UI data prefer $http({…}) — it is reactive and re-fetchable. Reach for raw fetch only for one-off, non-rendering calls.
Crypto / IDs crypto.randomUUID(), crypto.getRandomValues() Generate stable client-side ids and random values.
Locale Intl.NumberFormat, Intl.DateTimeFormat Locale-aware number, currency, and date formatting.
Storage localStorage, sessionStorage Prefer the friendlier $storage global ($storage.set, $storage.cookies.set) — it JSON-roundtrips and swallows quota errors.
Encoding atob, btoa, encodeURIComponent, decodeURIComponent Base64 and URI escaping.
Stdlib Math, JSON, Object, Array, Number, String, Date, Map, Set, RegExp, Promise, BigInt, Reflect, parseInt The everyday toolbox — no import needed.
DOM roots window, document, navigator For DOM APIs with no declarative equivalent (clipboard, observers, listeners).
Timers setTimeout, setInterval, clearTimeout, clearInterval Runtime-tracked — see Timers below.

Prefer the declarative primitive when there is one

The passthrough exists for APIs the declarative surface doesn’t cover. For reactive UI data use $http({…}) rather than raw fetch; for persistence use the $storage global rather than raw localStorage. Reach for a bare global only when there is no declarative equivalent.

Browser dialogs

alert, confirm, and prompt work exactly like plain JavaScript and are handy for a quick yes/no from an action handler. They block the thread while open, so for anything richer than a confirmation prefer a Modal or Drawer component bound to state (see the component catalog).

Live — opens a real browser dialog
$msg = "—"
function ask() { $msg = confirm("Proceed?") ? "confirmed" : "cancelled" }
function rename() { let n = prompt("New name?", "Atlas"); if (n) { $msg = "renamed to " + n } }
$app(Column([
  Text($msg, { variant: "large-heavy" }),
  Row([
    Button("Confirm", { onClick: ask }),
    Button("Rename", { variant: "ghost", onClick: rename })
  ], { gap: "sm" })
], { gap: "md" }))

Timers

setTimeout, setInterval, clearTimeout, and clearInterval behave like their JavaScript counterparts and return a handle you can later clear. The difference: the runtime tracks every timer and tears it down automatically when the program re-plans or the surrounding scope unmounts, so a stray setInterval can never outlive the program. You should still clear an interval you no longer need.

The canonical pattern is to create the timer inside an effect (not at the top level, which would re-create it on every render) and clear it in the effect’s cleanup:

$now = $util.now()

$effect(() => {
  let id = setInterval(() => { $now = $util.now() }, 1000)
  cleanup(() => clearInterval(id))
}, ["mount"])

clock = Text($util.formatDate($now, "time"))

For a debounce, clear and restart a setTimeout on each keystroke:

function onType(q) {
  clearTimeout($searchTimer)
  $searchTimer = setTimeout(() => { $query = q }, 300)
}

For a plain repeating effect, prefer the declarative $effect(…, ["every(1000)"]) trigger — reach for raw timers when you need an imperative handle, a one-shot delay, or a debounce/restart. See side effects for the full effect trigger grammar.

Recipes

Copy to clipboard

function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text)
    $toast = { kind: "success", message: "Copied" }
  } catch (err) {
    $toast = { kind: "error", message: err.message }
  }
}

Generate an id

function addRow() {
  $rows = [...$rows, { id: crypto.randomUUID(), label: "New row" }]
}

Format a number

function formatTotal(n) {
  $total = Intl.NumberFormat("en-US").format(n)
}

Read a file as a data URL

function pickAvatar(file) {
  const reader = new FileReader()
  reader.onload = () => { $avatarPreview = reader.result }
  reader.readAsDataURL(file)
}

Encode to Base64

function encode(text) {
  $encoded = btoa(text)
}

The synchronous helpers — crypto.randomUUID(), Intl.NumberFormat, and btoa — work directly in the preview, so this demo renders their results without any network access:

Live
$id = "—"
$money = "—"
$b64 = "—"
function run() {
  $id = crypto.randomUUID()
  $money = Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(1234567)
  $b64 = btoa("hello aktion")
}
$app(Column([
  Button("Run", { variant: "primary", onClick: run }),
  Text("id: " + $id),
  Text("money: " + $money),
  Text("base64: " + $b64)
], { gap: "sm" }))

Precedence & safety

The global passthrough is always tried last — after your state, bindings, actions, user-defined components, the curated standard library, and the component catalog. Author declarations and built-in components therefore always win over a same-named global: a library Text or Map component still beats the DOM Text node or the Map constructor, and your own $crypto shadows the host crypto. The passthrough only fills in names you haven’t defined, so it can never silently change the meaning of a name you control.

Next