Skip to main content

Headless or component?

OGrid ships two equally-supported entry points. Both are first-class. Both are MIT. Both expose the same spreadsheet features. Pick the one that fits the page you're building — you can mix them in the same app.

<OGrid> componentuseHeadlessGrid + spreadsheet hooks
What you writeOne JSX elementA handful of hook calls + your own <table> markup
Time to first working grid~5 lines~50-100 lines
Customization ceilingHigh (theme tokens, slots)Unlimited (you draw the chrome)
Design-system fitInherits via theme presets (shadcn, Fluent)Native — uses your chrome primitives directly
Spreadsheet featuresBuilt-inComposable from hooks
Best forStandard list pages, admin tables, dashboardsPages where chrome integration matters more than save-time

Use <OGrid> when…

  • You want a complete grid in one element. Sort, filter, paginate, edit, range select, fill handle, copy/paste, undo, keyboard nav — all wired.
  • The default chrome (via your chosen UI variant — Radix / Fluent) is close enough to your design system that a theme preset (preset-shadcn.css) can bridge the rest.
  • You're prototyping or building admin pages where shipping fast matters more than every pixel.
import { OGrid } from "@alaarab/ogrid-react-radix";
import "@alaarab/ogrid-react-radix/styles/index.css";
import "@alaarab/ogrid-react-radix/styles/preset-shadcn.css";

const columns = [
{ columnId: "name", name: "Name", sortable: true, editable: true },
{ columnId: "salary", name: "Salary", type: "numeric", sortable: true },
];

<OGrid columns={columns} data={employees} getRowId={(e) => e.id} />;

Use useHeadlessGrid + the spreadsheet hooks when…

  • You want OGrid's logic but rendered with your own table chrome (shadcn <Table>, Fluent <DataGrid>, plain HTML — anything).
  • The chrome integration is non-negotiable: a specific design system, custom row layouts, embedded widgets in cells, or shared <TableRow> components from the rest of the app.
  • You want to opt into spreadsheet features piece-by-piece. Add only inline edit on this page; add range selection + clipboard on that one.
import {
useHeadlessGrid,
useInlineEdit,
useRangeSelection,
useFillHandle,
useCellClipboard,
useUndoRedo,
useGridFocus,
} from "@alaarab/ogrid-react-radix";
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table"; // your shadcn primitives

const grid = useHeadlessGrid({ columns, data, getRowId: (r) => r.id });
const range = useRangeSelection({ rowCount: grid.rows.length, colCount: grid.columns.length });
const undo = useUndoRedo({ onCellValueChanged: applyEdit });
const edit = useInlineEdit({ columns, getRowId: (r) => r.id, onCellEdit: undo.onCellValueChanged });
const fill = useFillHandle({ rangeSelection: range, rows: grid.rows, columns, onFillCells });
const clipboard = useCellClipboard({ rangeSelection: range, rows: grid.rows, columns, onCellEdit });
const focus = useGridFocus({ rowCount: grid.rows.length, colCount: grid.columns.length, rangeSelection: range });

return (
<Table>
<TableHeader>{/* render grid.columns + grid.toggleSort + grid.sortIndicator */}</TableHeader>
<TableBody>{/* render grid.rows + edit / range / fill / clipboard / focus glue */}</TableBody>
</Table>
);

The full integration code is the SpreadsheetDemo Storybook story in react-radix — ~200 lines, copy-paste as your starter template.

Both at once — yes, in the same app

Mix freely. A common pattern:

  • Standard admin / list pages → <OGrid> (fast to build, sensible default UI)
  • Pages where the table is the product → the hooks (full control over the markup)

Same package import. Same theme tokens. Same TypeScript types. The spreadsheet features (inline edit, fill handle, clipboard, undo) work identically across both paths because they share the same core utilities (processClientSideData, applyFillValues, formatSelectionAsTsv, parseValue, etc).

What this means architecturally

<OGrid> is OGrid's reference chrome — a complete grid built on the same headless primitives any consumer would use. The two paths share underlying state machines (useOGridSorting, useOGridFilters, useOGridPagination, useOGridDataFetching); the only difference is who draws the table. That's why you can swap between them page-by-page without losing behavior parity, and why the bug fixes in one path automatically benefit the other.

Migrating between modes

There's no migration boundary. <OGrid> accepts the same column defs your headless code uses. Move a page from <OGrid> to headless by replacing the JSX element with hook calls + your own table; the column defs, the row data, the sort/filter/paginate behavior all stay identical. Move back the same way.