Grid: CSS-Style Layouts for React NativesteemCreated with Sketch.

in Steem Dev17 hours ago

Installation

All three components are available now:

npm install rn-markdown-editor

or

yarn add rn-markdown-editor

Grid

CSS Grid isn't a thing in React Native — there's no display: grid, no grid-template-areas, no browser layout engine doing the placement math for you. Most RN devs either fall back to manual flexDirection: "row" + flexWrap hacks that break the moment an item needs to span two columns, or give up and ship a plain list. Grid is built to close that gap: a Grid.Root/Grid.Item pair for spanning, area-based layouts, and a Grid.List for virtualized, data-driven grids — all sharing one responsive column/gap engine so they behave consistently no matter which one you reach for.


Grid.Root / Grid.Item

An unvirtualized grid container that supports column spanning, row spanning, named template areas, and both row- and column-major auto-flow — the closest RN gets to real CSS Grid.

How it works internally:

All placement math is isolated in a single pure function, computePlacements, so GridRoot itself never has to reason about collisions directly. For each item it resolves placement in priority order: a named area (matched against templateAreasresolveAreas walks the area grid once and merges every cell sharing a name into a single {rowStart, rowEnd, colStart, colEnd} rect, so an area can span an irregular block of cells); then an explicit column/row (1-based props converted to 0-based, with the column clamped so the item's span never overflows numColumns); then auto-placement. Auto-placement walks the grid cell by cell against an occupied set of "row,col" keys, and honors autoFlow's two independent axes — row vs column majorness, and a dense/sparse modifier that decides whether the search restarts from (0,0) on every item (dense, backfilling earlier holes) or resumes from the last cursor position.

Column-flow is the trickier of the two: real CSS column-flow needs a known row count to know when to wrap into the next column, so computePlacements uses a rowBound derived from templateRows.length for that — without templateRows it falls back to a generous 1000-row ceiling so placement still terminates, but the wrapping won't line up the way true column-flow would.

rowSpan has a hard constraint baked in: RN has no way to measure a sibling row's height before that row has rendered, so spanning rows is only possible when row heights can be known ahead of time — a numeric autoRows (every row is that many px) or an explicit templateRows array. GridRoot checks this once as canResolveRowHeights; if an item requests rowSpan without either being set, it's silently clamped back to 1 and a __DEV__-only console warning fires, rather than producing a layout that quietly lies about its own geometry.

That same canResolveRowHeights flag is also what decides which of two entirely different render paths GridRoot takes. When it's true and at least one item actually spans rows, GridRoot switches to absolute positioning: it precomputes a rowHeights array and cumulative rowTops offsets, then clones each child with an injected _gridStyle of { position: "absolute", left, top, width, height } computed straight from its placement — this is the only path that can render genuine multi-row spans. Otherwise it renders a plain flexWrap flow layout via buildFlowCells, which lays out each item's cell and then fills gaps with invisible, zero-height spacer Views so out-of-order area/explicit placement never leaves a visually broken hole — interior rows are always gap-filled this way, but the trailing row is only padded when compressSize is set, so a default grid doesn't force a half-empty last row to stretch across the full width. Either way, GridItem never computes its own size — it just renders whatever _gridStyle (an @internal, do-not-pass-manually prop) it was cloned with, alongside the caller's own style.

GridItem itself stays deliberately dumb: it's a React.memo'd wrapper around two nested Views, where the inner one defaults to flex: 1 (stretch to fill the reserved cell) but swaps to alignSelf: "flex-start" when inline is set — the cell still reserves its normal grid footprint so siblings don't reflow, but the content shrink-wraps instead of stretching, which is what you want for a badge or chip sitting inside a grid cell.

<Grid.Root
  columns={[{ width: 0, num: 1, gap: 0 }, { width: 641, num: 2, gap: 8 }, { width: 770, num: 3, gap: 8 }]}
  autoFlow="row dense"
  autoRows={120}
  templateAreas={[["hero", "hero", "side"], ["a", "b", "side"]]}
>
  <Grid.Item area="hero"><HeroCard /></Grid.Item>
  <Grid.Item area="side" rowSpan={2}><SideCard /></Grid.Item>
  <Grid.Item colSpan={2}><WideCard /></Grid.Item>
  <Grid.Item inline><Badge /></Grid.Item>
</Grid.Root>

Key props (Grid.Root):

PropTypeDefaultDescription
columnsGridBreakpoint[]Responsive { width, num, gap } rules; highest matching width wins
autoFlow`"row" \"column" \"row dense" \"column dense"`"row"Placement direction/strategy for items without column/row/area
autoRows`number \"auto"`"auto"Fixed row height (enables rowSpan) or content-sized rows
autoColumnsnumberFixed column width override (px); default splits width evenly
templateAreasstring[][]Named-area grid, e.g. [["header","header"],["side","main"]]
templateRowsnumber[]Per-row fixed heights; also bounds column-flow wrapping
variantGridVariantDensity preset (compact/comfortable/spacious) controlling default gap
compressSizebooleanfalsePad an incomplete trailing row with spacers instead of stretching it
scrollablebooleantrueWrap content in a ScrollView
showScrollbarbooleanfalseShow the native scroll indicator

Key props (Grid.Item):

PropTypeDefaultDescription
colSpan / rowSpannumber1Columns/rows to span; rowSpan needs autoRows (numeric) or templateRows
column / rownumberExplicit 1-based placement; skips auto-flow for this item
areastringMatches a cell name in Grid.Root's templateAreas
inlinebooleanfalseReserve the normal cell size but shrink content to fit instead of stretching

Grid.List

A virtualized, responsive-column grid built directly on FlatList, for when the grid is a data feed rather than a fixed layout.

How it works internally:

Because it's a thin layer over FlatList, all of FlatList's existing data ergonomics — onEndReached, refreshing/onRefresh, ListFooterComponent/ListHeaderComponent/ListEmptyComponent — pass straight through untouched. The tradeoff is spanning: FlatList virtualizes rows internally, so Grid.List deliberately does not support colSpan/rowSpan/templateAreas — the docs point you at dropping a small, non-virtualized Grid.Root in (e.g. inside ListHeaderComponent) for any layout that needs real spans.

FlatList can't change numColumns at runtime without a remount, so Grid.List forces one itself by keying the list off the resolved column count (`grid-list-${numColumns}`) — a responsive breakpoint change transparently rebuilds the list rather than silently failing to re-flow. Trailing-row handling mirrors Grid.Root's compressSize, but implemented for a flat array instead of a placement grid: when the item count doesn't divide evenly into numColumns, paddedData appends invisible filler entries — tagged with a private SPACER symbol so they can never collide with a real item's identity — until the last row is full; both keyExtractor and renderItem special-case that sentinel to render an inert, zero-content, pointerEvents="none" cell instead of calling the caller's renderItem.

Column width is resolved the same way Grid.Root resolves it: an onLayout measurement of the actual rendered width (falling back to useWindowDimensions before the first layout pass fires), minus gap * (numColumns - 1), split evenly across columns.

<Grid.List
  data={items}
  columns={[{ width: 0, num: 1, gap: 0 }, { width: 770, num: 3, gap: 8 }]}
  renderItem={({ item }) => <Card item={item} />}
  onEndReached={fetchMore}
  onEndReachedThreshold={0.4}
  ListFooterComponent={isLoading ? <Spinner /> : null}
  compressSize
/>

Key props (Grid.List):

PropTypeDefaultDescription
datareadonly T[]Source array
renderItem`({ item, index }) => ReactElement \null`Per-item renderer
columnsGridBreakpoint[]Same responsive rules as Grid.Root
compressSizebooleantruePad an incomplete trailing row so real items don't stretch to fill it
showScrollbarbooleanfalseShow the native scroll indicator
keyExtractor(item, index) => stringindex-basedStable key per item
onEndReached / onEndReachedThreshold0.4Pass-through FlatList pagination hooks
refreshing / onRefreshPass-through pull-to-refresh
ListHeaderComponent / ListFooterComponent / ListEmptyComponentPass-through FlatList slots

useGridConfig

The shared hook both Grid.Root and Grid.List call to resolve { numColumns, gap } for the current window width — kept as one hook specifically so the two components' responsive behavior can never drift apart.

How it works internally:

Breakpoints are min-width rules, same convention as the library's other responsive hooks: rules are sorted ascending by width, and the rule with the highest width that's still <= width wins — so a [{width:0,...},{width:641,...},{width:770,...}] list reads as "1 column by default, 2 from 641px, 3 from 770px." Gap resolution then falls through four levels in priority order: the matched breakpoint's own gap, then ThemeProvider's config.components.grid.variantGap[variant] (an app-wide override), then the library's built-in VARIANT_GAP[variant] default, then 0 if no variant was passed at all — so a consumer can re-tune "compact"/"comfortable"/"spacious" spacing globally without touching every Grid.Root/Grid.List call site individually.


Coin Marketplace

STEEM 0.04
TRX 0.33
JST 0.098
BTC 64506.36
ETH 1866.73
USDT 1.00
SBD 0.38