Skip to content

PlGrid

A twelve-column row and the cells in it. The column count and the two gutters live on the row; how many columns a cell takes lives on the cell, and it can change at every breakpoint.

React
tsx
import { PlGrid, PlGridItem } from 'plass-ui';

<PlGrid spacing={3}>
  <PlGridItem span={{ xs: 12, md: 8 }}>{main}</PlGridItem>
  <PlGridItem span={{ xs: 12, md: 4 }}>{aside}</PlGridItem>
</PlGrid>;
dart
import 'package:plass_ui/plass_ui.dart';

PlGrid(
  spacing: const PlassResponsive<double>(3),
  items: <PlGridItem>[
    PlGridItem(span: const PlassResponsive<int>(12, md: 8), child: main),
    PlGridItem(span: const PlassResponsive<int>(12, md: 4), child: aside),
  ],
);

Props

PropTypeDefaultDescription
columnsPlassResponsive<number>12How many columns a row is divided into. Every span and offset inside is read against this number
spacingPlassResponsive<number>2The gutter between items, on Tailwind's spacing scale — 4 is 1rem, and fractions are allowed
rowSpacing · columnSpacingPlassResponsive<number>The gutter on one axis only. Falls back to spacing
justify'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch'How a row distributes the space its items did not use
alignItems'start' | 'center' | 'end' | 'stretch' | 'baseline''stretch'How items sit against each other across the row
alignContent'start' | 'center' | 'end' | 'space-between' | 'space-around' | 'space-evenly' | 'stretch'Where the rows sit when the grid is shorter than the box holding it
wrapbooleantrueWhether a row that runs out of columns continues on the next one. Off gives one row that overflows
renderRenderPropRenders something other than a <div> (<section />, <ul />)
childrenReactNodeThe PlGridItems
PropTypeDefaultDescription
items * List<PlGridItem>The cells, as a list of descriptions rather than children — the grid has to know what each one takes to pack them into rows
columnsPlassResponsive<int>PlassResponsive(12)How many columns a row is divided into. Every span and offset inside is read against this number
spacingPlassResponsive<double>PlassResponsive(2)The gutter between cells, on Tailwind's spacing scale — 4 is 16 logical pixels, and fractions are allowed
rowSpacing · columnSpacingPlassResponsive<double>?The gutter on one axis only. Falls back to spacing
justifyPlassJustifyPlassJustify.startHow a row distributes the space its items did not use
alignItemsPlassAlignItemsPlassAlignItems.stretchHow items sit against each other across the row
alignContentPlassJustify?Where the rows sit when the grid is shorter than the box holding it
wrapbooltrueWhether a row that runs out of columns continues on the next one. Off gives one row, and that row scrolls sideways

PlGridItem

PropTypeDefaultDescription
spanPlassResponsive<number>How many of the grid's columns the item takes. A span wider than the row is clamped to it rather than overflowing
offsetPlassResponsive<number>0Columns left empty before the item — space pushed in ahead of it, not an absolute position in the row
alignSelf'auto' | 'start' | 'center' | 'end' | 'stretch' | 'baseline'Overrides the row's alignItems for this item alone
renderRenderPropRenders something other than a <div> (<li />, <article />)
childrenReactNodeWhat the cell holds
PropTypeDefaultDescription
child * WidgetWhat the cell holds
spanPlassResponsive<int>?How many of the grid's columns the item takes. A span wider than the row is clamped to it rather than overflowing
offsetPlassResponsive<int>?0Columns left empty before the item — space pushed in ahead of it, not an absolute position in the row
alignSelfPlassAlignSelf?Overrides the row's alignItems for this cell alone. There is no baseline here — a Flutter row is aligned on one baseline or on none

Every native <div> attribute passes straight through, on both.

PlGridItem is a description rather than a widget. The idiom this package already uses for an accordion's folds and a table's columns, and it is here for the same reason: the grid packs its members into rows by the columns they take, and a Widget is opaque. There is no asking one how wide it means to be.

Neither takes variant, color, elevation, size or density. A grid is the arrangement of the surfaces inside it, not a surface itself, and a cell that drew a sheet would make span a visual decision. There is no padding here either: the gutter round a page belongs to PlContainer and the padding round content to PlCard, and a grid with a track of its own would be a third one to keep in step. spacing is the only measurement it owns, and it is the space between cells.

Examples

span

A cell's width is span out of the row's columns, so span={6} is a half of the default twelve and a quarter of columns={24}.

A span wider than the row is clamped to the row rather than overflowing the page, which is what the caller meant. A cell with no span at all fills the row.

React

Responsive values

span, offset, columns and the three spacings all take a responsive value. Each entry applies from its own breakpoint up, so two of them usually describe a whole layout.

A bare value applies everywhere; a map is per breakpoint. span={{ xs: 12, md: 6 }} is full width on a phone and a half from 48rem.

A map is "from here up, use this instead". It is not "and nothing below": naming only md still leaves the prop's own default in force underneath, rather than silently dropping to whatever CSS would have fallen back to.

Dart has no union type to carry "a number or a map", so it is a PlassResponsive<T> with the base value positional and the overrides named: PlassResponsive<int>(12, md: 6) is full width on a phone and a half from 768. The positional base is what keeps the common case short, PlassResponsive(6) is the whole of "six columns everywhere", and it is also what makes "and nothing below" impossible to write by accident.

The breakpoint is resolved against the window rather than against the grid's own box, which is what a CSS media query measures: a grid nested three cards deep still changes shape at the same width as everything else on the screen.

The widths are Tailwind's own (sm 40rem, md 48rem, lg 64rem, xl 80remsm 640, md 768, lg 1024, xl 1280, with xs meaning from zero up), so a Plass grid and an md: utility change at the same moment. A rem against a 16px root and a logical pixel are the same length, so the two packages break at the same width.

React

offset

Columns left empty before the cell, space pushed in ahead of it, not an absolute position in the row. First in a twelve-column row, offset={4} with span={4} is the middle third; after a cell that already took four columns, the same offset skips four more and lands on the last third.

React

spacing

Tailwind's spacing scale, not Material's 8px one: a spacing of 4 is 1rem16 logical pixels, exactly what gap-4 already means and what the padding tables already use. Fractions are the point. 1.5 is 0.375rem. Every other number in this library is on that ladder, and a grid that measured its gutters differently from the card around it would be the one place a caller has to stop and convert.

rowSpacing and columnSpacing each override one axis and fall back to spacing.

The gutter comes out of the cell, not out of the row: two halves plus one gutter is still exactly the row's width, so a grid can sit flush against whatever is around it.

React

alignItems, alignContent and alignSelf

alignItems is how cells sit against each other across the row, and stretch is the default, which is what makes a row of cards the same height without anybody asking. alignSelf overrides it for one cell. alignContent is where the rows sit when the grid is shorter than the box holding it, and it is only ever visible on a grid with a height of its own.

There is no baseline on alignSelf. CSS resolves a baseline per item; a Flutter row is aligned on one baseline or on none, so a single cell cannot opt into one the row is not using. alignItems still takes it.

React

Nesting

A grid inside a cell, not a cell that is also a grid. The inner grid re-declares the column count for its own subtree while the cell around it keeps the width the outer grid gave it, which is what lets an eight-column region be divided into thirds without any arithmetic against the outer twelve.

Composition

PlGrid builds one Row per run inside a Column, and packs the runs by counting columns rather than by comparing widths. Every cell is a whole number of columns, so counting them cannot disagree with itself by a rounded pixel the way two doubles can.

A cell's width is a share of the row, so the row has to be measured before a cell can be built: that is the LayoutBuilder. The arithmetic is the React package's, written out ((width + gap) / columns × span − gap), and adding one gutter before dividing is what lets every cell give one back, so a row of spans that add up to the column count is exactly the width of the row.

Every run is laid out stretched inside an IntrinsicHeight, and each cell is then positioned inside the height it was given. That is what makes alignSelf expressible at all: Flutter has no per-child cross-axis alignment, so a cell that is not stretched is a full-height Column holding one child at one end of it.

Composition

PlGrid is a flex row and PlGridItem is a width in it, which is the shape a twelve-column grid has had since long before CSS had one of its own, and still the only shape where a cell can carry a start offset without an explicit line number, and where the row can be told to stop wrapping.

The three numbers a cell cannot know on its own, the column count and the two gutters, are handed down as inherited custom properties rather than through a React context. The values are responsive, and a media query can change an inherited custom property without React hearing about it, so the column count a cell lays itself out against is always the one on screen. A context would have to re-render the tree at every breakpoint to say the same thing.

The width itself is (100% + gap) × span / columns − gap, and it lives in the stylesheet rather than in a class name. columns is a number the caller picks and span changes at four widths, so the class would have to be assembled at runtime, and a class name assembled at runtime is a class name Tailwind never sees.

Accessibility

  • Neither element adds a role or a semantics node. A grid is an arrangement, and an arrangement is not something a screen reader should have to announce.
  • The document order is the reading order. offset moves a cell with a margin rather than reordering the row, so what a screen reader reads and what a sighted reader sees stay the same sequence.
  • render={<ul />} on the row and render={<li />} on the cells is how a grid of things that really are a list says so.

Differences from the React build

ReactFlutterWhy
<PlGridItem> childrenitems: List<PlGridItem>The grid packs its members into rows by the columns they take, and a Widget is opaque. Descriptions are the idiom PlAccordion and PlTable already use.
a bare value or a mapPlassResponsive<T>Dart has no union type. The base value is positional, so PlassResponsive(6) stays short.
alignSelf="baseline"CSS resolves a baseline per item; a Flutter row is aligned on one baseline or on none. alignItems still takes it.
justify="space-between"PlassJustify.spaceBetweenDart's enum values are lowerCamelCase. Same values, same meanings.
wrap={false} overflowswrap: false scrolls sidewaysA Flutter screen has no page-level overflow to leave the scrolling to, and an overflowing Row is a debug banner rather than something a reader can reach.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License