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

gridwhy flex has no answer
named areasflex has no second dimension
gridColumn / gridArea placementflex cannot place across rows and columns
overlapping cellsflex 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.