docs / patterns / dashboard-grid
DashboardGrid
a widget board the user arranges: drag to move, drag an edge to resize. widgets never overlap, and one dragged away leaves its hole rather than shuffling the rest around.
intensity · structuresrsc · client
provenance
shaped from sisu-pluslayer
core / patternsintensity
structures · organizes a region, stays out of the contentcomposition
each widget wraps a Panel or Card. the grid supplies position onlythe exported layout helpers drive an add-widget tray outside the gridnot for content that must grow. rowHeight is fixedno nesting a grid inside a grid itema11y
space lifts a focused widget, arrows move it a cell, escape cancels · every accepted move, resize and refusal lands in apolite live region · each control is named by its widget's labeljest-axedependencies
@dnd-kit/corelive demo · try it out
Credit trajectory
Credit trajectory
Upcoming deadlines
Upcoming deadlines
Grade trend
Grade trend
Timeline
Timeline
customize
columnsgrid width in cells
rowsgrid height in cells
rowHeightpixels per row
gapgutter in pixels
editingdrag, resize and remove; reveals the add tray
usage
import { DashboardGrid, DashboardGridItem, type GridItem } from "@usva-ui/react/patterns/dashboard-grid";
const [layout, setLayout] = useState<GridItem[]>(seed);
<DashboardGrid
rows={6}
layout={layout}
onLayoutChange={setLayout}
>
{layout.map((item) => (
<DashboardGridItem key={item.id} id={item.id} label={titles[item.id]}>
<Panel title={titles[item.id]}>{/* widget */}</Panel>
</DashboardGridItem>
))}
</DashboardGrid>DashboardGrid
| prop | type | default | notes |
|---|---|---|---|
| layout | GridItem[] | — | controlled. each entry is { id, x, y, w, h, minW?, minH?, maxW?, maxH? }. |
| onLayoutChange | (layout: GridItem[]) => void | — | fires only when a move or resize is accepted. a refused one never calls it. |
| columns | number | 10 | grid width in cells. |
| rows | number | 8 | grid height in cells. |
| rowHeight | number | 72 | pixels. rows never grow to fit their content. |
| gap | number | 16 | gutter in pixels. |
| editing | boolean | false | drag, resize and remove only exist while this is true. |
| keyboardInstructions | string | — | read to a screen reader when a widget is focused for dragging. |
DashboardGridItem
| prop | type | default | notes |
|---|---|---|---|
| id | string | — | must match a layout entry. an item with no entry renders nothing. |
| label | string | — | names the widget in every control's label and in the live region. |
| removable | boolean | true | set false to hide the remove button for a widget that must stay. |
layout helpers
| prop | type | default | notes |
|---|---|---|---|
| canPlace | (layout, item, bounds) => boolean | — | whether an item fits, ignoring the entry it came from. |
| findOpenSlot | (layout, item, bounds) => GridItem | null | — | first free position, scanning rows before columns. null when nothing fits. |
| addItem | (layout, item, bounds) => GridItem[] | — | findOpenSlot then append. returns the same array when there is no room. |
| removeItem | (layout, id) => GridItem[] | — | filter by id. |
| clampItem | (item, bounds) => GridItem | — | squeezes an item inside the grid and inside its own min and max. |
get it
bun add @usva-ui/reactusage
import { DashboardGrid, DashboardGridItem, type GridItem } from "@usva-ui/react/patterns/dashboard-grid";
const [layout, setLayout] = useState<GridItem[]>([
{ id: "trajectory", x: 0, y: 0, w: 4, h: 3, minW: 3 },
{ id: "upcoming", x: 4, y: 0, w: 3, h: 3 },
]);
<DashboardGrid layout={layout} onLayoutChange={setLayout} editing={editing}>
{layout.map((item) => (
<DashboardGridItem key={item.id} id={item.id} label={titles[item.id]}>
<Panel title={titles[item.id]}>{/* widget */}</Panel>
</DashboardGridItem>
))}
</DashboardGrid>