Core concepts

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 gridGrid([…], { columns })
Content centered on both axes (spinner, empty state, hero CTA)Center([…], { minHeight })
A 12‑column dashboard with mixed widthsGrid + GridItem(child, { span })
A centered, max‑width reading columnContainer([…], { size })
Direction that flips on mobile vs desktopStack([…], { 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.

Live
$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".

Live
$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 }).

Live
$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).

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

ModeHowBest for
Auto‑fit (default)omit columnsCard / KPI walls that reflow on their own — wraps as many ≥ minChildWidth columns as fit.
Fixedcolumns: N (1–12)Exactly N equal columns.
SpanGridItem children12‑track dashboards & sidebars with mixed widths.

Auto‑fit — reflows by itself

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

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

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

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

Live — resize the window / preview
$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" }))
Live — direction flips at 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.

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

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

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

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

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

PatternMeaning
sx: { p: "l", bg: "surface", radius: "lg" }Spacing, background, and radius from theme tokens.
sx: { px: "xl", ps: "m", me: "auto" }Logical spacingpx/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.
Live — hover a card
$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

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

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

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

GoalSnippet
Page bodyColumn([…], { gap: "lg" })
Toolbar (title left, actions right)Row([title, Spacer(), …buttons])
Even button rowRow([…], { gap: "sm" })
Equal-width cards in a rowGrid([…], { columns: 3 })
Reflowing KPI / card wallGrid([…], { minChildWidth: "200px" })
Sidebar + mainGrid([GridItem(s, { span: "1/4" }), GridItem(m, { span: "3/4" })])
Expand one child in a rowRow([StackItem(field, { grow: 1 }), button])
Center on both axesCenter([…], { minHeight: "60vh" })
Centered reading columnContainer([…], { size: "md" })
Stack on mobile, row on desktopStack([…], { direction: { base: "column", md: "row" } })
Padding / inset surfaceBox([…], { padding: "md", background: "muted" })
Full-width page bandSection([…], { background: "soft", title: "…" })
Two panes (content + aside)Split(left, right, { ratio: "2/3", divider: true })
Editorial bento gridBento([BentoCell(c, { span: 2 }), …])
Badge over an imageOverlay(base, [OverlayItem(b, { position: "top-right" })])
Group nodes without a boxFragment([a, b, c])
One-off style intent{ sx: { p: "l", bg: "surface", states: { hover: { scale: 1.03 } } } }

Next