Layout & flexbox

Compose Canvas with Row, Column, Container and Grid. Their semantic props express direction, spacing, alignment and bounds using the same React Native layout on iOS, Android and the web.

Direction

Row arranges siblings horizontally; Column stacks them vertically. Choose a gap with flush, tight, snug, cozy, relaxed or loose.
A
B
C
iOS
A
B
C
Android
A
B
C
Web
tsx
<Row cozy alignCenter>
<Emblem label="A" />
<Emblem label="B" />
<Emblem label="C" />
</Row>

Alignment

center, between and the other distribution props act along the layout's main axis. alignCenter, alignStart, alignEnd and baseline act across it. The example fills its parent, so space-between has real bounds to distribute.
A
B
C
iOS
A
B
C
Android
A
B
C
Web
tsx
<Row between alignCenter>
<Emblem label="A" />
<Emblem label="B" />
<Emblem label="C" />
</Row>

Sizing

A fill component beside a hugging sibling takes the remaining room. Use Container for a shared measure and span on a Row's direct children for proportional columns; the Row includes its gaps in the calculation.
Search
Connected
iOS
Search
Connected
Android
Search
Connected
Web
tsx
<Row snug alignCenter>
<Input label="Search" placeholder="Find a project" />
<Badge status success>Connected</Badge>
</Row>
Container step
Maximum width
xxxs
192px
xxs
256px
xs
320px
sm
384px
md
448px
lg
512px
xl
576px
xxl
672px
xxxl
768px
wide
896px
wider
1024px
widest
1152px
page
1280px
A named measure centers by default. Add start to pin it to the leading edge. Input and other measure-aware controls accept these same step booleans directly.

Responsive layout

Start with the desktop composition. Grid measures its own container to choose how many tiles fit. Row stacks changes direction at its own container breakpoint, preserving mounted controls, field values and focus.
Overview
Activity
Members
iOS
Overview
Activity
Members
Android
Overview
Activity
Members
Web
tsx
<Grid minTileWidth={220} columns={3} relaxed>
<Card><Typography>Overview</Typography></Card>
<Card><Typography>Activity</Typography></Card>
<Card><Typography>Members</Typography></Card>
</Grid>
Breakpoint
At this width and below
sm
640px
md
768px
lg
1024px
xl
1280px
2xl
1536px

Viewport tier

Window-level chrome reads the viewport tier: the window's width placed in a breakpoint bucket. A bucket applies at its width and below, and a width wider than every breakpoint, or one not known yet (a server render, the frame before layout), is base, the desktop variant. Consumers re-render only when the bucket changes, never per resize pixel. useFormFactor collapses the buckets into three tiers.
Form factor
Window width
Buckets
Phone
640px and below
sm
Tablet
641 to 1024px
md, lg
Desktop
Above 1024px
xl, 2xl, base
tsx
import { formFactor, responsive, useFormFactor, useResponsive, type FormFactor, type Responsive } from "@nannier/canvas";
// A value per bucket, composed in app code: base is the desktop case, and a
// breakpoint entry applies at that width and below.
const columns: Responsive<number> = { base: 3, lg: 2, sm: 1 };
// Pure pickers, for a width you already have.
responsive(390, columns); // 1
formFactor(390); // "phone"
// The hooks read the window and re-render only when its bucket changes.
function AppShell() {
const tier: FormFactor = useFormFactor(); // "phone" | "tablet" | "desktop"
const count = useResponsive(columns);
// ...
}
A server cannot measure a window, so it renders base. An app that knows it is serving a narrow client passes ThemeProvider's ssrBreakpoint (sm for a phone): the server and the hydration render use that bucket, and the real window's bucket applies right after.

Simulating a tier

BreakpointOverride pins the viewport bucket for a subtree, so a preview stage or a test can show the phone or tablet branch inside a desktop window. The form-factor switcher in each example's stage header on the web uses it. It renders nothing of its own.
tsx
import { BreakpointOverride, Container } from "@nannier/canvas";
// A preview stage or a test: everything under it resolves the phone tier.
// Constrain the width to match, so container-measured components agree.
<BreakpointOverride value="sm">
<Container sm>
<Screen />
</Container>
</BreakpointOverride>
// null clears the override, and the real window applies again.
<BreakpointOverride value={null}>
<Screen />
</BreakpointOverride>
  • It pins the bucket for every useBreakpoint, useResponsive and useFormFactor reading under it, the Sidebar and FilterPanel drawer modes and Popover's phone sheet included. Menus, dialogs and toasts opened from inside it resolve it too, wherever their overlay layer sits.
  • Components that measure their own container (DataTable, Grid, Row stacks, Tabs, Navbar) follow their real width. Constrain the subtree to the tier's width beside the override; their first frame, before they measure, already takes the simulated tier.
  • It pins the tier, not the geometry. A drawer or a phone sheet opened under it takes its phone presentation but lays out against the real window, so a simulated phone in a desktop window opens its sheet across the desktop window.
  • It is for previews and tests: a real app takes the tier from its window, never from an override.

Composition reference

Job
Canvas API
A reading or form measure
Container sm; add start for leading alignment
Two columns that stack
Row stacks with two Column span={6} children
Equal tiles that reflow
Grid minTileWidth={220} columns={3}
Content that can wrap
Row wrap
A child that takes spare space
Column fill
Copy that may shrink and wrap
Column shrink
Insets around a group
Column padTight, pad or padLoose