Grid Item

A bento grid of whole-cell tiles with aligned edges, and the tile with title, value and background parts.

Open Source

Installation

GridItem ships with the library — install it once, then import:

import { GridItem } from "@delacour/react-native-ui/grid-item";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add grid-item

Usage

app/index.tsx
import { GridItem } from "@/components/ui/grid-item";
import { Icon } from "@/components/ui/icon";
import { IconBluetooth, IconStorage, IconWifiFull } from "@/lib/icons/central";
import { View } from "react-native";

export default function Example() {
  return (
    <View className="flex-1 justify-center p-4">
      <GridItem.Group columns={4} gap={12}>
        <GridItem colSpan={2} rowSpan={2} onPress={() => {}}>
          <GridItem.Media>
            <Icon icon={IconStorage} />
          </GridItem.Media>
          <GridItem.Title>Storage</GridItem.Title>
          <GridItem.Value>74 GB</GridItem.Value>
          <GridItem.Description>of 100 GB used</GridItem.Description>
        </GridItem>
        <GridItem size="sm">
          <GridItem.Media>
            <Icon icon={IconWifiFull} />
          </GridItem.Media>
          <GridItem.Title>Wi-Fi</GridItem.Title>
        </GridItem>
        <GridItem size="sm">
          <GridItem.Media>
            <Icon icon={IconBluetooth} />
          </GridItem.Media>
          <GridItem.Title>Bluetooth</GridItem.Title>
        </GridItem>
        <GridItem colSpan={2}>
          <GridItem.Title>Battery</GridItem.Title>
          <GridItem.Value>82%</GridItem.Value>
        </GridItem>
      </GridItem.Group>
    </View>
  );
}

Anatomy

<GridItem.Group>
  <GridItem>
    <GridItem.Background />
    <GridItem.Actions />
    <GridItem.Media />
    <GridItem.Title />
    <GridItem.Value />
    <GridItem.Description />
    <GridItem.Footer />
  </GridItem>
</GridItem.Group>

How cells work

The group measures its own width and divides it into columns tracks with a gap between them. A cell is one track wide and, by default, as tall as it is wide. Each direct child takes colSpan by rowSpan cells and is placed first-fit in the order it is written — never in a hole earlier than the tile before it — so screen-reader order is writing order.

Dashboard
columns={4}
┌───────────┬─────┬─────┐
│           │ 1×1 │ 1×1 │
│    2×2    ├─────┼─────┤
│           │ 1×2 │ 1×1 │
├───────────┴──┐  ├─────┘
│      3×1      │  │
└───────────────┴──┘

Edges are snapped to the pixel grid, so neighbouring tiles never gain a hairline between them, and the group is exactly as tall as the rows it used. Tiles are positioned on the logical start inset, so the grid mirrors under right-to-left with no code of its own.

A tile wrapped in your own component loses its spans — the group reads colSpan and rowSpan from its direct children's props. Accept them on the wrapper and pass them through. A direct child that is not a GridItem is placed as 1×1, or by its own colSpan / rowSpan props.

Variants

default is a card, outline a border, muted a fill and plain nothing. Every variant reserves the same one-point border, so switching variant never moves the content. Set it once on the group, or per tile.

Variants
Variants
<GridItem.Group variant="muted">
  <GridItem variant="outline">…</GridItem>
</GridItem.Group>

Sizes

default and sm set the padding, the type scale and the media's icon size. A tile without its own takes the group's.

Quick actions
Quick actions
<GridItem.Group size="sm">…</GridItem.Group>

Pressable tiles

A tile with an onPress (or onLongPress) renders as a Pressable: it is announced as a button, scales gently on press and passes haptic through. Without one it is a plain view. isDisabled dims the tile and stops the press.

Pressable tiles
<GridItem haptic="selection" onPress={toggleLock}>
  <GridItem.Title>Front door</GridItem.Title>
  <GridItem.Description>{isLocked ? "Locked" : "Unlocked"}</GridItem.Description>
</GridItem>

Backgrounds

GridItem.Background fills the tile behind its content and clips to its corner — a chart, a gradient, an oversized glyph. It is drawn first wherever it is written, and takes no touches, so a pressable tile stays pressable across its whole face.

Backgrounds
Backgrounds
<GridItem colSpan={2}>
  <GridItem.Background className="top-1/3">
    <Chart className="flex-1" config={config} data={data} frameClassName="flex-1" xKey="day">
      <Chart.Area yKey="steps" />
    </Chart>
  </GridItem.Background>
  <GridItem.Title>Steps</GridItem.Title>
  <GridItem.Value>10,400</GridItem.Value>
</GridItem>

Row sizing

aspect makes a cell's height follow its width — 1 is square, 2 is twice as wide as tall — so the grid keeps its shape at any column count. rowHeight fixes the height in points instead. Pass one or the other, never both.

<GridItem.Group columns={3} aspect={1.5}>…</GridItem.Group>
<GridItem.Group columns={3} rowHeight={56}>…</GridItem.Group>

Before its first measure the group reserves its height — from rowHeight, or an aspect ratio — and keeps its tiles invisible, so nothing jumps when they appear. A later change of width or column count glides each tile to its new cell, and jumps instead under reduce motion.

Clipping and large text

Tile heights are whole cells, never content-sized: the grid knows its height before anything loads, so a tile cannot push its neighbours. Content taller than its cell clips. At large text sizes give the rows more room with rowHeight, or ask for fewer columns — useGridItemGroup() returns the measured cellHeight to decide with.

const columns = PixelRatio.getFontScale() > 1.3 ? 2 : 4;

GridItem, Item or Kpi

GridItemItemKpi
ShapeA tile sized by its group's cellsA row: media, text, actions on one lineA free-standing card around one number
Sized byThe groupIts contentIts content
Use it forDashboards, quick actions, home screensLists and settingsOne metric, its change and a sparkline

A Kpi can sit inside a tile. For hundreds of uniform cells, reach for a list with numColumns: the group is for tens of tiles on one screen and does not virtualise.

Accessibility

The group is a list. Each tile is one stop, labelled by joining its title, value and description, unless you pass accessibilityLabel — do that when a part holds elements rather than text. Media, backgrounds and actions are not read.

API Reference

GridItem.Group

Extends React Native's ViewProps.

Prop

Type

GridItem

Extends Pressable's props.

Prop

Type

GridItem.Background

Extends ViewProps. Absolute-fill, behind the content, no touches.

GridItem.Media

Extends ViewProps. Icons inside inherit the tile's icon size and foreground colour.

GridItem.Title, GridItem.Value, GridItem.Description

Extend TextProps. Each carries its own colour and scale; their text joins the tile's accessibility label.

GridItem.Footer

Extends ViewProps. Pushed to the bottom of the tile.

GridItem.Actions

Extends ViewProps. Pinned to the tile's top-end corner.

useGridItem

Prop

Type

useGridItemGroup

Prop

Type

On this page