Modules & multi-file apps.
A single program is one flat list of statements — great for a
streamed reply, cramped for a real app. Modules let you
split an app across many .aktion files that
import and export components, variables,
functions, and reactive $state, with
JavaScript-like syntax and true per-file
scope. The files are linked in the browser —
no build step — so the playground and
any web page work the same way.
A first split
Here is a two-file app. app.aktion is the entry —
it holds the $app(…) root and pulls in a component
declared in Greeting.aktion. Editing either file re-links the
whole program; the preview shows the linked result.
import { Greeting } from "./Greeting.aktion"
$app(Greeting("Aktion"))
export function Greeting(name) {
return Card([
CardHeader(`Hello, ${name}!`, { subtitle: "Rendered from an imported file" })
])
}
Exporting
Prefix any top-level declaration with export to make it
importable from another file. Anything not exported is private to
its file. Every binding kind can be exported — components, actions,
hooks, reactive $state, and plain bindings:
// components.aktion
export function Card2({ title }) { // a component
return Card([CardHeader(title)])
}
export function track(name) { // an action (lowercase)
console.log(name)
}
export function $useToggle() { // a hook ($-prefixed)
$on = $state(false)
return { on: $on, toggle: () => { $on = !$on } }
}
export $count = 0 // a reactive atom
export accent = "var(--rui-primary)" // a plain binding
export { a, b } lists, re-exports, and
export of a destructuring declaration are not supported —
export each binding inline, where it is declared.
Importing
Import named bindings with import { … } from "…".
Rename with as, and import reactive state by keeping its
$ sigil on both sides:
import { Card2 } from "./components.aktion"
import { Card2 as PanelCard } from "./components.aktion" // aliased
import { $count, increment } from "./store.aktion" // a shared atom + an action
import { $count as $total } from "./store.aktion" // $ stays across `as`
Names are matched by their bare name, so export $count lines up
with import { $count }. A $state import must keep
its $ across as ({ $a as $b }) —
mixing { $a as b } is a syntax error. from and
as are ordinary identifiers everywhere else, so existing code
that uses them as variable names keeps working.
True per-file scope
Each file is its own scope. A file's non-exported top-level names are
private, so two files can reuse the same name without
clashing — the linker renames each file's private symbols behind the
scenes. Only the names you import cross the boundary.
// Button.aktion
icon = "bolt" // private to this file
export function PrimaryButton({ label, onClick }) {
return Button(label, { variant: "primary", icon: icon, onClick: onClick })
}
// Link.aktion
icon = "arrow-up-right-from-square" // a DIFFERENT, private `icon`
export function LinkButton({ label, onClick }) {
return Button(label, { variant: "ghost", icon: icon, onClick: onClick })
}
Built-in library components (Button, Card,
Column, …) and runtime globals ($util,
route, console) are resolved by the runtime, not by
a module, so they are always available without importing — and never
renamed.
Sharing reactive state
Export a $state atom and the files that import it read and write
the same reactive cell. This is the simplest way to share
state across an app — a tiny store in its own file:
import { $count, increment } from "./store.aktion"
$app(Column([
Card([
CardHeader("Shared store"),
Text(`Count: ${$count}`),
Button("Increment", { variant: "primary", onClick: increment })
])
], { gap: "lg", align: "center", padding: "xl" }))
export $count = 0
export function increment() {
$count = $count + 1
}
The entry file keeps its own names canonical: the
$state atoms you declare or import into app.aktion
are exactly the names that serializeState(),
hydrateState(), and applyDelta() target.
Where modules resolve from
A specifier can be relative, absolute (from the project root), or a full URL. URL modules are fetched over the network when the project is linked, so you can pull a shared component straight from a CDN or gist:
import { Button } from "./Button.aktion" // same folder
import { Card2 } from "../components/Card2.aktion" // parent folder
import { Nav } from "/layout/Nav.aktion" // from the project root
import { Hero } from "https://example.com/ui/Hero.aktion" // remote, fetched on link
A relative import inside a URL module resolves against that URL, so a remote
file can pull in its own neighbours. Bare specifiers
(import { x } from "lodash") have no meaning and are reported as
unresolved.
The entry file
Linking starts from one entry file —
app.aktion in the playground. The entry is what defines the
rendered root (a $app(…) statement, or the legacy
aktion = … binding); imported files normally only
declare and export. The linker walks every import from
the entry, merges the whole graph into one program, and the runtime renders
it — import cycles are fine (each file is merged once).
In the playground
The playground is multi-file. Use the
file explorer on the left to create, rename, switch, and
delete files; app.aktion is always the entry. Press
to download the whole project
as a .zip, or the share / download buttons to get a
single linked program (every file inlined). Examples and shared links load
into app.aktion. Try the
“Multi-file modules” preset to start from a
ready-made split.
Lazy loading & code-splitting
Modules organise your source; the Lazy(…) helper
defers rendering. Reach for Lazy(…) when a part
of the UI should mount only when it is needed (showing a fallback until a
promise resolves), and reach for modules to keep that part — and the
rest of the app — in readable, reusable files.
// In app.aktion
import { Dashboard } from "./Dashboard.aktion"
$app(Column([
Header(),
// Render the heavy view behind a fallback until it's ready.
Lazy(() => Dashboard(), { fallback: Skeleton({ lines: 6 }) })
]))
Best practices
- One component per file (named to match), so imports read like a manifest.
- Keep shared
$statein its own store file and export the atoms plus the actions that mutate them — importers get a consistent surface. - Keep the entry thin. Let
app.aktionwire pieces together and own the$app(…)root; push real UI into imported files. - Export only what other files need. Everything else stays private — that is what makes a name safe to reuse.
- Prefer relative imports within a project; reserve URL imports for genuinely shared, versioned components.
Next steps
Open the playground
Create files, import between them, and download the project as a zip.
Launch playground → ReferenceLanguage
Program structure, $state, expressions, and control flow.
Global state
Patterns for sharing reactive state across an app.
Read the guide →