Grid
A multi-column grid that wraps children into equal-width columns. Built on top of Row, so all Box spacing and alignment props are available.
Usage
import { Card, Grid, Typography } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, justifyContent: 'center', padding: 16 }}>
<Grid columns={2} gap="sm">
<Card variant="outlined"><Typography>Item 1</Typography></Card>
<Card variant="outlined"><Typography>Item 2</Typography></Card>
<Card variant="outlined"><Typography>Item 3</Typography></Card>
<Card variant="outlined"><Typography>Item 4</Typography></Card>
</Grid>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}
How It Works
Each child is wrapped in a cell with flexBasis: ${100 / columns}%, so all columns are equal width. The gap prop is implemented as gutters rather than by shrinking cells: each cell takes half the gap as horizontal padding, and the row cancels the outer half on both edges with a negative margin. flexBasis stays exactly 100 / columns%, so the row occupies the full width of its parent and the outermost cells stay flush with its edges.
Different Column Counts
import { Button, Grid, IconButton, Typography } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, justifyContent: 'center', padding: 16, gap: 24 }}>
<Typography variant="titleSmall">Two columns</Typography>
<Grid columns={2} gap="md">
<Button variant="filled" onPress={() => {}}>A</Button>
<Button variant="outlined" onPress={() => {}}>B</Button>
</Grid>
<Typography variant="titleSmall">Four columns</Typography>
<Grid columns={4} gap="sm">
<IconButton icon="heart" accessibilityLabel="Heart" onPress={() => {}} />
<IconButton icon="star" accessibilityLabel="Star" onPress={() => {}} />
<IconButton icon="bell" accessibilityLabel="Bell" onPress={() => {}} />
<IconButton icon="cog" accessibilityLabel="Settings" onPress={() => {}} />
</Grid>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}
Responsive Columns
columns also accepts a map of window size classes. The grid
re-resolves the count when the window crosses a breakpoint. A map cascades down
to the nearest smaller breakpoint, so you only set the widths where the count
changes.
import { Card, Grid, Typography } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, justifyContent: 'center', padding: 16 }}>
<Grid columns={{ compact: 1, medium: 2, expanded: 4 }} gap="sm">
<Card variant="outlined"><Typography>Item 1</Typography></Card>
<Card variant="outlined"><Typography>Item 2</Typography></Card>
<Card variant="outlined"><Typography>Item 3</Typography></Card>
<Card variant="outlined"><Typography>Item 4</Typography></Card>
</Grid>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}
The map requires a compact key, so every width resolves to a value. Use
useBreakpointValue from @rootnative/core directly when a value other than
the column count must follow the same breakpoints.
Spanning Cells
A plain child always spans one column. Wrap a child in Grid.Cell to make it
span more, for asymmetric layouts such as a main area with a sidebar. The
span prop also accepts a breakpoint map, and an oversized span is clamped to
the column count, so it fills the row instead of overflowing it.
import { Card, Grid, Typography } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, justifyContent: 'center', padding: 16 }}>
<Grid columns={12} gap="sm">
<Grid.Cell span={8}>
<Card variant="outlined"><Typography>Main</Typography></Card>
</Grid.Cell>
<Grid.Cell span={4}>
<Card variant="outlined"><Typography>Aside</Typography></Card>
</Grid.Cell>
<Grid.Cell span={{ compact: 12, medium: 6 }}>
<Card variant="outlined"><Typography>Half on tablets</Typography></Card>
</Grid.Cell>
</Grid>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}
Grid.Cell is also exported as GridCell for direct imports. It only sizes
itself inside a Grid — rendered anywhere else it warns in development and
falls back to full width.
Staggered entrances
A <Stagger> from @rootnative/inertia can wrap the grid's children directly.
Grid looks through it, so each child still gets its own cell and still
inherits its own delay:
import { Grid } from '@rootnative/components'
import { Motion, Stagger } from '@rootnative/inertia'
<Grid columns={{ compact: 1, medium: 2 }} gap="md">
<Stagger interval={60}>
{items.map((item) => (
<Motion.View
key={item.id}
initial={{ opacity: 0, translateY: 12 }}
animate={{ opacity: 1, translateY: 0 }}
>
<Card>{/* … */}</Card>
</Motion.View>
))}
</Stagger>
</Grid>
Put the Stagger inside the Grid, not around it. A Grid wrapped in a
Stagger is a single child of that stagger, so every card shares one delay and
the cascade disappears.
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
columns | number | BreakpointValues<number> | - | Yes | Number of equal-width columns, or a breakpoint map of column counts. A map cascades down to the nearest smaller breakpoint, so `{ compact: 1, expanded: 4 }` gives 1 column below 840dp and 4 at or above. |
wrap | boolean | false | No | Whether children should wrap to the next line when they overflow. |
inverted | boolean | false | No | Reverses the layout direction (`row-reverse`). |
p | SpacingValue | - | No | Padding on all sides |
px | SpacingValue | - | No | Horizontal padding (paddingStart + paddingEnd) |
py | SpacingValue | - | No | Vertical padding (paddingTop + paddingBottom) |
pt | SpacingValue | - | No | Padding top |
pb | SpacingValue | - | No | Padding bottom |
ps | SpacingValue | - | No | Padding start (left in LTR, right in RTL) |
pe | SpacingValue | - | No | Padding end (right in LTR, left in RTL) |
m | SpacingValue | - | No | Margin on all sides |
mx | SpacingValue | - | No | Horizontal margin (marginStart + marginEnd) |
my | SpacingValue | - | No | Vertical margin (marginTop + marginBottom) |
mt | SpacingValue | - | No | Margin top |
mb | SpacingValue | - | No | Margin bottom |
ms | SpacingValue | - | No | Margin start |
me | SpacingValue | - | No | Margin end |
gap | SpacingValue | - | No | Gap between children |
rowGap | SpacingValue | - | No | Row gap between children |
columnGap | SpacingValue | - | No | Column gap between children |
flex | number | - | No | Flex value |
align | enum | - | No | Align items along the cross axis |
justify | enum | - | No | Justify content along the main axis |
bg | string | - | No | Background color |
hrefAttrs | { target?: string; rel?: string; download?: string | boolean; } | - | No | Web only. The `target`, `rel` and `download` of the `<a>` that a view with an `href` renders, for example under Expo Router's `<Link asChild>`. `target: '_blank'` opens the link in a new tab. Native ignores it. |
onKeyDown | (event: { nativeEvent: { key?: string; }; }) => void | - | No | - |
Grid.Cell
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
span | number | BreakpointValues<number> | 1 | No | Number of grid columns this cell spans, or a breakpoint map of spans. Clamped to the parent Grid's column count, so an oversized span fills the row instead of overflowing it. |
hrefAttrs | { target?: string; rel?: string; download?: string | boolean; } | - | No | Web only. The `target`, `rel` and `download` of the `<a>` that a view with an `href` renders, for example under Expo Router's `<Link asChild>`. `target: '_blank'` opens the link in a new tab. Native ignores it. |
onKeyDown | (event: { nativeEvent: { key?: string; }; }) => void | - | No | - |