Layout.
Three primitives cover almost every layout you will ever build:
Column stacks things top‑to‑bottom,
Row lays them out left‑to‑right, and
Grid arranges equal columns and card walls.
Reach for Center, Container, Box,
Spacer, and GridItem spans for the rest. No raw
CSS, no flexbox bookkeeping — just composition.
The mental model
Every layout in Aktion is a tree of containers. Pick the container by the question you are answering:
| You want… | Use |
|---|---|
| Things stacked vertically (a page, a card body, a form) | Column([…]) |
| Things side by side (a toolbar, a row of buttons, label + value) | Row([…]) |
| Equal columns / a reflowing card or KPI grid | Grid([…], { columns }) |
| Content centered on both axes (spinner, empty state, hero CTA) | Center([…], { minHeight }) |
| A 12‑column dashboard with mixed widths | Grid + GridItem(child, { span }) |
| A centered, max‑width reading column | Container([…], { size }) |
| Direction that flips on mobile vs desktop | Stack([…], { direction: {base, md} }) |
gap and padding use the spacing scale
none · 3xs · 2xs · xs · sm · md · lg · xl · 2xl · 3xl
(none = 0).
Wherever you see a value below, it can also be a
responsive map like { base: "sm", md: "lg" }.
Column — stack vertically
Column is the workhorse: it is the usual page body and the
usual card body. Children stretch to the full width; control spacing with
gap and horizontal placement with align.
$app(Column([
PageHeader("Project Atlas", { subtitle: "Updated 2 hours ago" }),
Card([CardHeader("Overview"), Text("A Column stacks its children top to bottom with an even gap.")]),
Card([CardHeader("Activity"), Text("Each card is a child; the Column owns the spacing between them.")])
], { gap: "lg" }))
Row — lay out horizontally
Row places children left‑to‑right at their
natural width, vertically centered — exactly what
you want for toolbars, button groups, and label/value pairs. Push items to
opposite edges with a Spacer() or
justify: "between".
$app(Card([Row([
Text("Team members", { variant: "large-heavy" }),
Spacer(),
Button("Invite", { variant: "ghost", icon: "user-plus" }),
Button("New", { variant: "primary", icon: "plus" })
], { gap: "sm" })]))
Need equal‑width children (e.g. two cards sharing a row)? Pass
grow: true — or, better for true grids, use
Grid({ columns }). Need one child to expand while the rest hug
their content (a search box beside a button)? Wrap it in
StackItem(child, { grow: 1 }).
$app(Row([
StackItem(Input("q", { placeholder: "Search the docs…" }), { grow: 1 }),
Button("Search", { variant: "primary" })
], { gap: "sm" }))
Distributing children with justify
justify controls spacing along the main axis:
start (default), center, end,
between, around, evenly.
align controls the cross axis
(start/center/end/stretch).
$app(Column([
Card([Row([Badge("A"), Badge("B"), Badge("C")], { justify: "between" })]),
Card([Row([Badge("A"), Badge("B"), Badge("C")], { justify: "center" })]),
Card([Row([Badge("A"), Badge("B"), Badge("C")], { justify: "end" })])
], { gap: "md" }))
Grid — equal columns & card walls
Grid has three modes. Pick by intent:
| Mode | How | Best for |
|---|---|---|
| Auto‑fit (default) | omit columns | Card / KPI walls that reflow on their own — wraps as many ≥ minChildWidth columns as fit. |
| Fixed | columns: N (1–12) | Exactly N equal columns. |
| Span | GridItem children | 12‑track dashboards & sidebars with mixed widths. |
Auto‑fit — reflows by itself
$app(Grid([
StatCard("Revenue", { value: "$48.2k", trend: "up", delta: "+12%" }),
StatCard("Users", { value: "2,184", trend: "up", delta: "+184" }),
StatCard("Churn", { value: "1.4%", trend: "down", delta: "-0.3%" }),
StatCard("NPS", { value: "62", trend: "up", delta: "+5" })
], { gap: "md", minChildWidth: "180px" }))
Fixed columns
$app(Grid([
Card([CardHeader("One")]),
Card([CardHeader("Two")]),
Card([CardHeader("Three")])
], { columns: 3, gap: "md" }))
Span mode — GridItem on a 12‑track grid
Wrap children in GridItem(child, { span }) to give them
explicit widths on a 12‑column track. span is a number
(1–12) or a fraction ("1/2", "1/3",
"3/4"). Any GridItem child turns on the 12‑track
grid automatically — this is the canonical sidebar‑plus‑main layout.
$app(Grid([
GridItem(Card([CardHeader("Side"), Text("span 1/4")]), { span: "1/4" }),
GridItem(Card([CardHeader("Main"), Text("span 3/4")]), { span: "3/4" })
], { gap: "lg" }))
Center — both axes at once
Center is the easy way to drop a spinner, empty state, or hero
call‑to‑action into the middle of a region. Give it
minHeight to reserve vertical space, or axis to
center on only one axis.
$app(Card([Center([
EmptyState("No messages yet", { description: "Start a conversation to see it here.", icon: "inbox" })
], { minHeight: "240px" })]))
Responsive layouts
Any of gap, align, justify,
padding, Grid's columns, and
Stack's direction accept a responsive map keyed by
breakpoint: base (mobile‑first),
sm 640px, md 768px, lg 1024px,
xl 1280px. Use Stack (not Row/Column)
when the direction itself must change — e.g. a sidebar that
stacks on mobile and sits beside the content on desktop.
$app(Grid([
Card([CardHeader("Tile 1")]), Card([CardHeader("Tile 2")]),
Card([CardHeader("Tile 3")]), Card([CardHeader("Tile 4")])
], { columns: { base: 1, sm: 2, lg: 4 }, gap: "md" }))
$app(Stack([
Card([CardHeader("Nav"), Text("Column on mobile, beside the content on desktop.")]),
Card([CardHeader("Content"), Text("Stack is the escape hatch for responsive direction.")])
], { direction: { base: "column", md: "row" }, gap: "md" }))
Spacing & surfaces
Container([…], { size }) centers a wide page within a
comfortable reading width (sm/md/lg/xl/full).
Box([…], { padding, background, border, maxWidth }) is a
plain spacing/surface wrapper for when a full Card is too heavy.
Spacer() flexes to push siblings apart inside a Row;
with a size it becomes a fixed gap.
$app(Column([
Box([Text("A subtle inset surface — lighter than a Card.")], { padding: "md", background: "muted", border: "subtle" }),
Box([Text("Tinted callout-style box.")], { padding: "md", background: "primary" })
], { gap: "md" }))
Section — full-width page bands
Section([…], { background, width, title?, subtitle?, eyebrow? })
is a marketing‑style band: a full‑bleed background with its content
centered to a comfortable width. Stack a few to build a landing page.
background is one of base/soft/surface/muted/brand.
$app(Column([
Section([
Button("Get started", { variant: "primary" })
], { background: "brand", eyebrow: "New", title: "Ship UIs in one language", subtitle: "A band that spans full width and centers its content." }),
Section([
Grid([Card([Text("Fast")]), Card([Text("Reactive")]), Card([Text("Themeable")])], { columns: 3, gap: "md" })
], { background: "soft", title: "Features" })
]))
Split — two panes
Split(left, right, { ratio, divider, stackAt, sticky }) places two
panes side by side with a ratio (e.g. "1/3"), an
optional divider line, a stackAt breakpoint where it
collapses to a column, and an optional sticky pane.
$app(Split(
Column([CardHeader("Docs"), Text("Long-form content on the left.")]),
Card([CardHeader("On this page"), Text("A sticky aside on the right.")]),
{ ratio: "2/3", divider: true, stackAt: "md" }
))
Bento — named-span grid
Bento([…]) of BentoCell(child, { span, rowSpan })
builds an editorial “bento box” grid where cells claim different
column/row spans — ideal for feature showcases.
$app(Bento([
BentoCell(Card([CardHeader("Hero"), Text("Spans two columns")]), { span: 2 }),
BentoCell(Card([CardHeader("Tall"), Text("Spans two rows")]), { rowSpan: 2 }),
BentoCell(Card([Text("A")])),
BentoCell(Card([Text("B")]))
], { columns: 3}))
Overlay & Fragment
Overlay(base, [OverlayItem(child, { position })]) layers content
absolutely over a base node — badges, ribbons, captions. Positions are
named corners/edges (top-right, bottom-left, …).
Fragment([…]) groups siblings without a layout box
(display: contents), so a custom component can return several
nodes straight into a parent Grid or Row.
$app(Overlay(
Card([Image({ src: "https://picsum.photos/400/200", alt: "Cover" })]),
[
OverlayItem(Badge("Sale", { tone: "danger" }), { position: "top-right" }),
OverlayItem(Text("Featured", { variant: "small-heavy" }), { position: "bottom-left" })
]
))
The sx styling channel
Every component accepts a universal sx prop — a
bounded, theme‑safe style intent (not raw CSS). Values are
token references, enums, or sanitised scalars, so it stays XSS‑safe and
LLM‑enumerable. sx also accepts responsive maps and
interaction states.
| Pattern | Meaning |
|---|---|
sx: { p: "l", bg: "surface", radius: "lg" } | Spacing, background, and radius from theme tokens. |
sx: { px: "xl", ps: "m", me: "auto" } | Logical spacing — px/mx emit padding-inline/margin-inline and ps/pe/ms/me set the inline start/end sides, so RTL apps mirror automatically. |
sx: { width: { base: "100%", md: "50%" } } | Responsive map → real @media rules at sm/md/lg/xl. Works on every key (spacing, color, flex, position, typography, …). |
sx: { fontSize: "2xl", weight: "700", textDecoration: "underline" } | Bounded typography — fontSize token ramp (xs…4xl) or a length, weight 100–900. |
sx: { states: { hover: { scale: 1.03, shadow: "lg" } } } | Interaction states: hover/focus/active/disabled/focus-visible/checked/group-hover. |
sx: { p: "safe", minHeight: "dvh" } | Safe‑area insets (safe/safe-top/…) and dynamic viewport units. |
sx: { bg: "gradient.brand" } | A named theme gradient as the background. |
sx: { bgImage: "/hero.jpg", bgOverlay: "rgba(0,0,0,.4)" } | Background image (http(s)/relative/data:image URLs only) with an optional color or gradient.* overlay wash — hero banners with readable text. |
sx: { position: "sticky", top: 0, zIndex: "modal" } | Positioning + layer tokens — zIndex names resolve through themeable --rui-z-* vars ($theme({ zIndex: {…} })). |
animate: "fade-up" · animate: { preset: "zoom", delay: 150 } | Sibling motion prop on every component — presets like fade/fade-up/zoom/slide-*/pulse/float. The object form tunes delay/duration/repeat. Auto‑respects prefers-reduced-motion. |
$app(Grid([
Card([Text("Hover me")], { sx: { p: "l", states: { hover: { scale: 1.04, shadow: "lg" } } } }),
Card([Text("Gradient")], { sx: { p: "l", bg: "gradient.cool", color: "#fff" } }),
Card([Text("Half on md+")], { sx: { p: "l", width: { base: "100%", md: "60%" } } })
], { columns: 3, gap: "md" }))
Sticky & its stuck state
Sticky(child, { top }) pins a node as the page scrolls. Once
pinned it sets data-stuck="true" on itself, so a CSS hook (a
shadow, a hairline) can react to the pinned state — a common pattern for
condensing a header on scroll.
Sticky(
Row([Text("Title", { variant: "large-heavy" }), Spacer(), Button("Action")]),
{ top: "0" }
)
// while pinned the node carries data-stuck="true"
Recipes for complex layouts
Dashboard — header, KPI strip, content grid
$app(Column([
PageHeader("Sales", { subtitle: "Q4 2026", actions: [Button("Export", { variant: "ghost", icon: "download" })] }),
Grid([
StatCard("MRR", { value: "$48.2k", trend: "up", delta: "+12%" }),
StatCard("Deals", { value: "318", trend: "up", delta: "+24" }),
StatCard("Win rate", { value: "31%", trend: "down", delta: "-2%" })
], { gap: "md" }),
Grid([
GridItem(Card([CardHeader("Pipeline"), Text("Main chart goes here.")]), { span: "2/3" }),
GridItem(Card([CardHeader("Tasks"), Text("Side panel.")]), { span: "1/3" })
], { gap: "lg" })
], { gap: "lg" }))
Holy‑grail / app shell — header, sidebar, content
For a real multi‑page app prefer the dedicated
AppShell(sidebar, content) pattern. To assemble it by hand,
nest a Row inside a Column:
$app(Column([
Card([Row([Text("Acme", { variant: "large-heavy" }), Spacer(), Badge("Pro", { tone: "primary" })])]),
Grid([
GridItem(Card([CardHeader("Menu"), Text("Sidebar nav")]), { span: "1/4" }),
GridItem(Column([
Card([CardHeader("Welcome back")]),
Card([CardHeader("Recent activity")])
], { gap: "md" }), { span: "3/4" })
], { gap: "lg" })
], { gap: "md" }))
Centered auth card
$app(Center([
Box([Column([
CardHeader("Sign in", { subtitle: "Welcome back" }),
Input("email", { placeholder: "you@example.com" }),
Input("password", { placeholder: "Password", type: "password" }),
Button("Continue", { variant: "primary" })
], { gap: "md" })], { padding: "lg", background: "surface", border: "default", maxWidth: "360px" })
], { minHeight: "70vh" }))
Cheat sheet
| Goal | Snippet |
|---|---|
| Page body | Column([…], { gap: "lg" }) |
| Toolbar (title left, actions right) | Row([title, Spacer(), …buttons]) |
| Even button row | Row([…], { gap: "sm" }) |
| Equal-width cards in a row | Grid([…], { columns: 3 }) |
| Reflowing KPI / card wall | Grid([…], { minChildWidth: "200px" }) |
| Sidebar + main | Grid([GridItem(s, { span: "1/4" }), GridItem(m, { span: "3/4" })]) |
| Expand one child in a row | Row([StackItem(field, { grow: 1 }), button]) |
| Center on both axes | Center([…], { minHeight: "60vh" }) |
| Centered reading column | Container([…], { size: "md" }) |
| Stack on mobile, row on desktop | Stack([…], { direction: { base: "column", md: "row" } }) |
| Padding / inset surface | Box([…], { padding: "md", background: "muted" }) |
| Full-width page band | Section([…], { background: "soft", title: "…" }) |
| Two panes (content + aside) | Split(left, right, { ratio: "2/3", divider: true }) |
| Editorial bento grid | Bento([BentoCell(c, { span: 2 }), …]) |
| Badge over an image | Overlay(base, [OverlayItem(b, { position: "top-right" })]) |
| Group nodes without a box | Fragment([a, b, c]) |
| One-off style intent | { sx: { p: "l", bg: "surface", states: { hover: { scale: 1.03 } } } } |
Next
Component catalog
Every component, prop, and a live preview — including all the layout primitives.
Browse components → Try itPlayground
Paste any snippet above and tweak it live with autocomplete and inspect mode.
Open the playground → ReferenceLanguage
State, expressions, control flow, and the rules behind component calls.
View reference →