DashboardGrid

A 12-column widget board for overview screens. Each widget declares a span in twelfths, and the grid measures its OWN width (never the window) to pick a tier: a wide container honors every span, a narrower one reflows widgets to their narrowSpan, and a phone-sized one stacks them full width.
Cells render bare, because a widget arrives with its own surface (a Card, a Chart, a Stats row) and a second frame around it would only double up. A widget's title is its accessible name rather than a header the grid paints: customize mode uses it to name the widget's drag grip and its drop zone.
Locked, it is a plain static grid and no drag machinery is mounted at all; unlocked turns on customize mode, where every cell gains a grip and the board reorders by pointer, keyboard, or screen reader through the kit's own drag-and-drop. The order is a plain array of ids, controlled through order and onOrderChange so the app can persist it, or left to the grid with defaultOrder.
The board takes its widgets through items, the collection prop every other collection-taking component in the kit uses; the original widgets spelling still works as a deprecated alias and warns in development.
Revenue
Mon
Tue
Wed
Thu
Signups
Slowest endpoints
iOS
Revenue
Mon
Tue
Wed
Thu
Signups
Slowest endpoints
Android
Revenue
Mon
Tue
Wed
Thu
Signups
Slowest endpoints
Web
tsx
<DashboardGrid
items={[
{ id: "revenue", span: 8, title: "Revenue", content: <Chart title="Revenue" data={[{ label: "Mon", value: 12 }, { label: "Tue", value: 18 }, { label: "Wed", value: 15 }, { label: "Thu", value: 22 }]} /> },
{ id: "signups", span: 4, title: "Signups", content: <BarList title="Signups" items={[{ label: "Web", value: 82 }, { label: "iOS", value: 64 }]} /> },
{ id: "endpoints", span: 12, title: "Slowest endpoints", content: <BarList title="Slowest endpoints" items={[{ label: "/api/search", value: 240 }, { label: "/api/feed", value: 180 }]} /> },
]}
/>

Props

Prop
Description
items
DashboardWidget[]
The widgets to lay out, and the canonical collection prop. Their array order is the fallback order: any widget whose id is missing from the active order is appended, so adding one to a release never breaks a saved layout.
widgets
DashboardWidget[]
Deprecated alias for `items`, kept working so existing call sites are untouched. Pass `items` instead: it is the spelling every collection-taking component in the kit uses (Sidebar, StackedList, DescriptionList, Feed, Stats). `items` wins if both are passed.
order
string[]
CONTROLLED widget order, as ids. The primary path: the consuming app owns the array and persists it through its own API, applying each `onOrderChange`. Ids the widget list does not hold are ignored, and widgets missing from it are appended, so a stored order survives a release that adds or drops a widget.
defaultOrder
string[]
Initial order for UNCONTROLLED use; the grid then applies each drop itself. Defaults to the `items` array order.
onOrderChange
(order: string[]) => void
Fired with the full, reconciled next order after every drop that changes it, in BOTH the controlled and the uncontrolled mode.
unlocked
boolean
Customize mode: cells gain the edit affordance and a drag grip, and the board becomes reorderable. The grid never flips this itself; the app owns the "Done" / "Customize" control. Locked (the default) the drag provider is not mounted at all.
storageKey
string
Web-only convenience for the UNCONTROLLED path: seed the initial order from browser storage under this key and write each drop back to it. Ignored when `order` is passed (the controlled order owns persistence). On native there is no storage, so the order is session-only there; `clearStoredDashboardOrder` forgets a saved layout. A server-rendered board renders `defaultOrder` first and adopts the saved layout in the commit right after hydration, so React always hydrates against the markup the server actually sent.
compact
boolean
Density axis (pass none for the default density).
testID
string
E2E hook forwarded to the root element.
style
StyleProp<ViewStyle>
Outer layout composition only (width/flex within a parent), never a restyle hook.

Do & Don’t

The order lives in the consumer's data

Don’t
Signups
Revenue
Don't pass a controlled order and drop the change on the floor: the drop completes and announces, and then the widget snaps back because nothing updated the array.
Do
Signups
Revenue
Treat order as the single source of truth and persist every onOrderChange through your own API, so a layout survives a reload on any device.

title names the widget for assistive tech

Don’t
Signups
Revenue
Don't pass a slug, an id, or a placeholder. The board looks identical, and that is the trap: a keyboard or screen-reader user in customize mode is left dragging "widget-2" around with nothing to tell them which tile it is.
Do
Signups

1,204

Revenue
Give every widget the title its own header already shows. Nothing paints it, but customize mode reads it out: the grip announces "Reorder Signups", and moving the widget announces which one moved and what it landed against.

Widgets bring their own surface

Don’t
1,204
Don't hand-roll the widget frame out of a styled View. That rebuilds Card at the call site, drifts from the kit's radius and elevation, and is exactly the shim the bare cell exists to avoid.
Do
Signups

1,204

Revenue
Hand each widget a component that already carries a surface. The cell is bare on purpose, so a Card, a Chart, a Stats row, or a BarList lands with exactly one frame around it.