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:
- Inside a
functionbody (an action / event handler). - Inside an
effectbody (a declarative side effect). - Inside a lambda — including the multi-statement
(x) => { …; return … }form.
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.
| Group | Globals | Notes |
|---|---|---|
| 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).
$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:
$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
Side effects
The effect trigger grammar, cleanup, timers, and watched atoms.
Actions
The full function body grammar — state, HTTP, navigate, emit, return values.
Read the guide → ReferenceLanguage reference
Reactive state, expressions, control flow, and the $util helper namespace.