@hypergrid/react
v0.0.0
Published
React bindings for hypergrid: SSR-correct CSS Grid renderer + headless useGrid hook with drag, resize, drop-from-outside, keyboard a11y.
Maintainers
Readme
@hypergrid/react
React bindings for hypergrid: an SSR-correct <Grid> renderer and a headless useGrid hook with drag, resize, drop-from-outside, and keyboard accessibility built in.
Install
pnpm add @hypergrid/reactRequires React 18 or 19.
Quick start (headless hook)
"use client";
import { useState } from "react";
import { useGrid, type GridLayout } from "@hypergrid/react";
export function Dashboard({ initial }: { initial: GridLayout }) {
const [layout, setLayout] = useState<GridLayout>(initial);
const grid = useGrid({
layout,
cols: 12,
rowHeight: 60,
gap: 12,
onLayoutChange: setLayout,
});
return (
<div ref={grid.containerRef} {...grid.containerProps}>
{layout.map((item) => (
<div key={item.id} {...grid.getItemProps(item.id)}>
<span {...grid.getHandleProps(item.id)}>⋮⋮</span>
{item.id}
<span {...grid.getResizeHandleProps(item.id, "se")} />
</div>
))}
</div>
);
}The hook returns prop bags you spread onto the elements you already render. You own the markup and styling; the hook owns the layout math, gestures, and accessibility.
Server rendering
useGrid is client-only (gestures need a browser), but layout placement isn't. Render your items with grid.containerProps and grid.getItemProps(id) and the server HTML already has the final grid-column / grid-row strings — no hydration flash, no measurement during render.
If you don't need interactivity at all (read-only dashboards, share previews), use the <Grid> component, which works in Server Components:
import { Grid, type GridLayout } from "@hypergrid/react";
export function ReadOnlyGrid({ layout }: { layout: GridLayout }) {
return (
<Grid layout={layout} cols={12} rowHeight={60} gap={12}>
{({ item, style, "data-hypergrid-item": id }) => (
<div key={item.id} data-hypergrid-item={id} style={style}>
{item.id}
</div>
)}
</Grid>
);
}Options
useGrid({
layout, // GridLayout — controlled
cols, // number of columns
rowHeight, // px per row
gap, // px between cells (default 0)
onLayoutChange, // (next: GridItem[]) => void
preventCollision, // reject moves that would overlap (default false)
compaction, // "vertical" | "horizontal" | "tight" | "none" (default "vertical")
onDrop, // (payload, position) => void — called when a drop source lands
animateLayoutChanges, // FLIP transitions on commit (default true)
animationDuration, // ms (default 180)
});Drag, resize, drop
Three handle prop bags — attach them to whatever element you like:
<span {...grid.getHandleProps(item.id)}>⋮⋮</span>
<span {...grid.getResizeHandleProps(item.id, "se")} />
<button {...grid.getDropSourceProps({ kind: "chart" }, { w: 6, h: 4 })}>
Add chart
</button>getHandleProps(id)— drag handle. Active item getsdata-hypergrid-dragging="true"on its container so you can style it.getResizeHandleProps(id, "se")— resize handle. Only"se"(south-east) is implemented today; other directions are on the roadmap.getDropSourceProps(payload, size)— palette item. Drag it onto the grid andonDropfires with your payload and the final{ x, y, w, h }.
While a drop is in flight, grid.dropPreview is a { x, y, w, h } you can render as a ghost element so the user can see where the item will land:
{grid.dropPreview ? (
<div
className="drop-ghost"
style={{
gridColumn: `${grid.dropPreview.x + 1} / span ${grid.dropPreview.w}`,
gridRow: `${grid.dropPreview.y + 1} / span ${grid.dropPreview.h}`,
}}
/>
) : null}Keyboard accessibility
Every handle is focusable and operable from the keyboard:
| Key | What it does | | ---------------------------------- | --------------------------------------------------- | | Tab | Focus the next handle | | Space / Enter | Pick up or commit | | ↑ ↓ ← → | Move (drag handle) or grow / shrink (resize handle) | | Escape | Revert to the position at pickup |
On a drop source, Space places the item at the compactor's first available slot.
Screen-reader announcements
Every gesture publishes a human-readable string on grid.announcement. Render it into an ARIA live region so screen readers narrate what just happened:
<div role="status" aria-live="polite" className="sr-only">
{grid.announcement}
</div>The string changes on pickup, on each cell crossing, on commit, and on revert — covering both pointer and keyboard gestures.
Animation
By default, non-active items animate from their old visual position to the new one using a FLIP transition on every commit. Animations are skipped during an active pointer gesture (they'd stack and jitter) and re-enabled at gesture boundaries, drops, and keyboard moves.
To turn it off:
useGrid({ ..., animateLayoutChanges: false });Or shorten:
useGrid({ ..., animationDuration: 120 });Types
Re-exported from @hypergrid/core:
import type {
GridItem,
GridLayout,
GridConfig,
CompactionMode,
Compactor,
} from "@hypergrid/react";Hook-specific:
import type {
UseGridOptions,
UseGridResult,
GridContainerProps,
GridItemProps,
GridHandleProps,
GridResizeHandleProps,
GridDropSourceProps,
ResizeDirection,
DropPosition,
} from "@hypergrid/react";License
MIT
