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-plus

layer

core / patterns

intensity

structures · organizes a region, stays out of the content

composition

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 item

a11y

space lifts a focused widget, arrows move it a cell, escape cancels · every accepted move, resize and refusal lands in a polite live region · each control is named by its widget's labeljest-axe

dependencies

@dnd-kit/core
live demo · try it out
Credit trajectory
Credit trajectory
Upcoming deadlines
Upcoming deadlines
Grade trend
Grade trend
Timeline
Timeline
customize
columnsgrid width in cells
10
rowsgrid height in cells
6
rowHeightpixels per row
72
gapgutter in pixels
16
editingdrag, resize and remove; reveals the add tray
usagecopy
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

proptypedefaultnotes
layoutGridItem[]controlled. each entry is { id, x, y, w, h, minW?, minH?, maxW?, maxH? }.
onLayoutChange(layout: GridItem[]) => voidfires only when a move or resize is accepted. a refused one never calls it.
columnsnumber10grid width in cells.
rowsnumber8grid height in cells.
rowHeightnumber72pixels. rows never grow to fit their content.
gapnumber16gutter in pixels.
editingbooleanfalsedrag, resize and remove only exist while this is true.
keyboardInstructionsstringread to a screen reader when a widget is focused for dragging.

DashboardGridItem

proptypedefaultnotes
idstringmust match a layout entry. an item with no entry renders nothing.
labelstringnames the widget in every control's label and in the live region.
removablebooleantrueset false to hide the remove button for a widget that must stay.

layout helpers

proptypedefaultnotes
canPlace(layout, item, bounds) => booleanwhether an item fits, ignoring the entry it came from.
findOpenSlot(layout, item, bounds) => GridItem | nullfirst 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) => GridItemsqueezes an item inside the grid and inside its own min and max.

get it

bun add @usva-ui/reactcopy
usagecopy
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>