Grid
CSS Grid, said in the same style props as everything else
Features
display is the layout mode. Stacks name the axis; the mode is a prop.
Every grid track and placement property is a style prop, so it themes, responds and compiles.
Grid is native on web. On native there is no grid engine, and this page says where that bites.
Gui has always had flexbox. It now has CSS Grid on the same footing: display
is the mode, and every grid property is an ordinary style prop, so it takes
tokens, media queries and the optimizing compiler like any other.
Neither mode is privileged. Reach for whichever expresses the layout.
import { View, XStack } from '@hanzo/gui'// flex — one axis, items flow<XStack gap="$4"><Item /><Item /><Item /></XStack>// grid — two axes, explicit tracks<View display="grid" gridTemplateColumns="repeat(3, 1fr)" gap="$4"><Item /><Item /><Item /></View>
The mode is a prop, the axis is the component
XStack and YStack name an axis — across, and down. They have never
meant “flexbox”; they set flexDirection and nothing else. So a stack takes a
mode like anything else:
<XStack display="grid" gap="$4"> // across, on grid<YStack display="grid" gap="$4"> // down, on grid
That works because the axis is resolved against the mode. It is worth knowing
why, because the raw CSS does not do this for you: flex-direction is inert
under grid, and the two engines invert the same word — flex row runs across,
while grid’s grid-auto-flow: row fills rows and therefore runs down. Written
by hand, display: grid on a flex row silently lays out vertically. XStack
says the axis in both vocabularies, so it stays horizontal.
A stack you do not pass display to emits no extra class and is byte-identical
to what it always was.
The same layout, both ways
Four layouts, each expressed in both modes. Where they are equivalent, prefer whichever reads better.
A row of three
<XStack gap="$4">…</XStack><View display="grid" gridTemplateColumns="repeat(3, 1fr)" gap="$4">…</View>
Flex sizes each child to its content and distributes the slack; grid makes three
tracks of exactly 1fr. For equal columns, grid says it directly.
A column
<YStack gap="$4">…</YStack><View display="grid" gap="$4">…</View>
Identical. Grid with no template fills one implicit column top to bottom, which is a column.
A gallery that reflows
<XStack flexWrap="wrap" gap="$4"><View minW={160} flexGrow={1} />…</XStack><View display="grid" gridTemplateColumns="repeat(auto-fit, minmax(160px, 1fr))" gap="$4">…</View>
Both reflow. The grid version needs no per-child sizing and no media query —
auto-fit decides the track count from the available width. Measured: two
tracks at 390px, four at 768px, eight at 1440px.
This is the one case where the two engines disagree, so it is worth knowing
exactly where. They are pixel-identical while every row is full. They part on
a partial last row: flex’s flexGrow stretches a lone trailing item across
the whole width, while grid holds it to its track. Measured with seven items
over three tracks, only the orphan differs, and only in width — by 328px on a
480px container.
Neither is wrong; they answer different questions. Grid keeps the rhythm, which
is usually what a gallery wants. If you want the flex behaviour, say so —
gridColumn="1 / -1" on the last child spans it, and justifyItems controls
how a track’s content fills it.
A sidebar and a body
<XStack gap="$4"><View w={240} /><View flexGrow={1} /></XStack><View display="grid" gridTemplateColumns="240px 1fr" gap="$4"><View /><View /></View>
Equivalent. Grid puts the sizes in one place instead of on each child.
What grid does that flex cannot
Flexbox is one-dimensional. These have no flex equivalent, which is the reason to reach for grid at all.
Named areas — the layout is legible as a picture:
<View display="grid" gridTemplateAreas={`"nav head" "nav body"`} gridTemplateColumns="240px 1fr" gridTemplateRows="auto 1fr" gap="$4" ><View gridArea="nav" /><View gridArea="head" /><View gridArea="body" /></View>
Explicit placement — a child claims its own span, independent of source order:
<View display="grid" gridTemplateColumns="repeat(4, 1fr)" gap="$4"><View gridColumn="1 / 3" /><View gridColumn="3 / 5" /><View gridColumn="1 / 5" /></View>
Overlap — two children in one cell, no absolute positioning:
<View display="grid"><Image gridArea="1 / 1" /><Overlay gridArea="1 / 1" placeSelf="end" /></View>
Dense packing — gridAutoFlow="dense" backfills holes left by items that
span more than one track.
Native
Grid is genuinely native on web: real CSS Grid, no polyfill and no JavaScript layout pass.
React Native has no grid engine — its own display admits only none,
flex and contents, and Yoga implements no grid. So on native, display: grid becomes flex and no grid property crosses at all — not the tracks,
not the areas, not the placement, not even gridAutoFlow. Not approximated:
dropped, because a value an engine cannot read is worse applied than absent.
display is the only one that crosses. Everything one-dimensional survives on
the element’s own flex properties, which is why the idiom below works.
That is the whole rule, and it is blunter than it looks. Measured, a three-column grid on native:
{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' } → { display: 'flex' }
No tracks, and no flexDirection either — so React Native’s own default takes
over and you get one full-width column. A gallery written as tracks alone
is a column on a phone.
Writing one layout that works on both
Say the axis in both vocabularies. Each is inert on the other engine, so they compose on one element with no branch:
<View display="grid" gridTemplateColumns="repeat(auto-fit, minmax(160px, 1fr))" flexDirection="row" flexWrap="wrap" gap="$3" >
Web reads the tracks and ignores the flex props; native reads the flex props
and never sees the tracks. One source, both engines, no $platform branch.
What cannot cross at all
| grid | why flex has no answer |
|---|---|
| named areas | flex has no second dimension |
gridColumn / gridArea placement | flex cannot place across rows and columns |
| overlapping cells | flex cannot put two children in one cell |
gridAutoFlow="dense" | nothing to backfill in one dimension |
These are web-only compositions. Reach for them deliberately and give native its own arrangement.
Two more words that invert
flexDirection is not the only prop that changes meaning across the boundary.
justifyContent and alignItems are named relative to flexDirection in flex,
but in grid justify-* is always inline and align-* always block. They agree
for a flex row and swap for a flex column — so on YStack, ZStack or
a bare View, the same prop moves the other axis. And flexWrap is inert under
grid: where flex would wrap, grid adds implicit tracks and overflows.
The top three cover most real layouts — galleries, card decks, uniform columns —
so grid is usually the better way to write them even in a cross-platform app.
The bottom four are web-only compositions. Reach for them deliberately, and give
native its own arrangement with $platform-native when you do.
Properties
Tracks and placement take the CSS value verbatim, unitless, exactly as the stylesheet would:
grid · gridTemplate · gridTemplateColumns · gridTemplateRows ·
gridTemplateAreas · gridAutoColumns · gridAutoRows · gridAutoFlow ·
gridArea · gridColumn · gridColumnStart · gridColumnEnd · gridRow ·
gridRowStart · gridRowEnd
Alignment splits by who is aligned. alignItems, alignContent and
justifyContent mean the same as they do in flex. Grid adds the ones flex has
no use for:
justifyItems · justifySelf · placeItems · placeContent · placeSelf
Spacing is gap, columnGap and rowGap — the same props, the same tokens, in
both modes.