Layout
Column, Row and Box are Compose's own layout primitives, kept because a phone lays out with them. Scaffold adds a top bar, a bottom bar, and the padding between them.
Column
Stack children vertically:
<Column class="gap-16 p-20">
<Text>First</Text>
<Text>Second</Text>
<Text>Third</Text>
</Column> Row
Arrange children horizontally:
<Row class="gap-8 items-center">
<Icon name="lucide:star" />
<Text>4.5</Text>
<Text class="text-muted-foreground">(120 reviews)</Text>
</Row> See it happen
Row and Column take the same alignment and gap classes, but which axis each one governs depends on which component it lands on. Switch between these to see the mapping flip.
<Row class="items-center gap-8"> <Text>★</Text> <Text>4.5</Text> </Row>
On a Row, items-center is the cross axis — vertical — so it becomes verticalAlignment, and gap-8 is the main axis, so it becomes horizontalArrangement.
Alignment and arrangement
items-* is the cross axis, justify-* the main axis — Row and
Column read the same classes and flip which is which. The full table is in Classes.
FlowRow and FlowColumn
A Row never wraps — its children stay on one line, however many there are. FlowRow wraps onto a new line when it runs out of width; FlowColumn is the same down the other axis:
<FlowRow class="gap-8">
@for (tag in tags) {
<Badge>{tag}</Badge>
}
</FlowRow> Box
Stack children on top of each other:
<Box class="w-200 h-200 bg-primary">
<Image src={url} />
<Text class="text-white p-16">Caption</Text>
</Box> Overlay
<Box>
<Image src={photo} />
<Box class="w-full h-full" background="v-gradient(transparent 60%, #000)" />
<Text class="text-white p-16">Overlay text</Text>
</Box> Centered content
<Box class="w-full h-full items-center justify-center">
<Spinner />
</Box> Scaffold
A top bar, a bottom bar, and the content between them, padded so neither overlaps it:
<Scaffold
topBar={<TopAppBar scroll="pin" />}
bottomBar={<BottomBar>…</BottomBar>}
>
<Column class="gap-16 p-16">
<Text>Main content</Text>
</Column>
</Scaffold> topBar's scroll is "hide", "collapse" or "pin" — how the bar behaves as the content beneath it scrolls.
Scaffold in a layout
A root layout is the usual place for one, wrapping <slot />:
// src/routes/+layout.wh
<Scaffold topBar={<TopAppBar scroll="pin" />}>
<slot />
</Scaffold> TopLayer
The layer a <dialog> or a popover renders into: above the
page and every screen, wherever it is written. It draws nothing — a dialog is markup on top
of it, and the scrim, the card and their motion are yours:
<TopLayer open={open} onClose={() => open = false} blur={4}>
<Box class="w-full h-full bg-black/10" onClick={() => open = false} transition:fade />
<Column class="w-full mx-16 bg-background rounded-xl p-24" transition:scale={{ to: 0.95 }}>
<Text>Saved</Text>
</Column>
</TopLayer> modal is true by default, as showModal() is: the page
takes no presses and back calls onClose. modal={false} is popover="auto": the page stays live, and a press outside every child calls onClose. blur is dp of blur on the page behind.
Each child stacks in one centered cell, as a <dialog>'s children do; self-end puts one at the bottom, justify-self-end at the right. A
child's in: and out: play as the layer opens and closes, and the
keyboard takes room from the layer, so a centered card rises above it.
Common patterns
Header, scrollable content, footer
<Column class="w-full h-full">
<Row class="safe-top px-16 py-12 bg-primary">
<Text class="text-white text-xl">Header</Text>
</Row>
<Column class="flex-1 p-16" scrollable>
<Text>Scrollable content</Text>
</Column>
<Row class="safe-bottom p-16 bg-muted">
<Button variant="secondary">Cancel</Button>
<Box class="flex-1" />
<Button>Save</Button>
</Row>
</Column> Side by side
<Row class="gap-16 p-16">
<Column class="flex-1">
<Text>Left column</Text>
</Column>
<Column class="flex-1">
<Text>Right column</Text>
</Column>
</Row> Card grid
<Column class="p-16 gap-16" scrollable>
<Row class="gap-16">
<Card class="flex-1">Item 1</Card>
<Card class="flex-1">Item 2</Card>
</Row>
<Row class="gap-16">
<Card class="flex-1">Item 3</Card>
<Card class="flex-1">Item 4</Card>
</Row>
</Column> Root layouts and safe areas
Android draws under the status bar. A root +layout.wh needs class="safe", or the linter asks for bleed instead to say the
overlap is deliberate:
// src/routes/+layout.wh
<Column class="w-full h-full safe">
<TopBar />
<slot />
</Column> One edge, or the keyboard, at a time:
<Column class="safe-top"> <!-- below the status bar -->
<Row class="safe-bottom"> <!-- above the navigation bar -->
<Column class="safe-ime"> <!-- above the keyboard --> Classes and props share one scale: p-16 is the same 16dp as p={16}.
A prop overrides a class on the same element, so class="p-16" p={20} lands at 20dp.
Universal onClick
onClick works on any element — Box, Column, Row, an Image — not only on Button. The compiler
adds .clickable where Compose needs it:
<Card onClick={() => $navigate("/product/${id}")}>
<Image src={image} />
<Text>{name}</Text>
</Card> The modifier escape hatch
modifier={…} takes any expression of type Modifier, for
the part no class or prop reaches. It composes with a component's own modifiers rather
than replacing them:
<Box modifier={Modifier.weight(1f)} class="w-full">
<slot />
</Box> Imports are worked out from the chain; a modifier the compiler does not recognize is a build error naming it and the import.