# Data Grid **Category**: react **URL**: https://ui.darkcode.dev/en/docs/react/components/data-grid **Source**: https://raw.githubusercontent.com/DarkCode-Developers/darkcode-ui/refs/heads/main/apps/docs/content/docs/en/react/components/(data-display)/data-grid.mdx > A full-featured data grid with sorting, selection, column resizing, pinned columns, drag-and-drop row reorder, expandable rows, virtualization, and async loading — built on the Table component. *** ## Import ```tsx import { DataGrid } from '@darkcode-ui/react'; ``` ### Usage The `DataGrid` component takes a flat `data` array, a `columns` definition, and a `getRowId` function. It renders a fully accessible table with built-in support for sorting, selection, column resizing, and more. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Button, Chip, DataGrid, Dropdown, Label} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; interface Payment { id: string; customer: string; email: string; transactionId: string; status: "succeeded" | "processing" | "refunded" | "failed"; amount: number; fee: number; method: string; } const payments: Payment[] = [ { amount: 2450, customer: "Emma Wilson", email: "emma@example.com", fee: 73.5, id: "1", method: "Visa •••• 4242", status: "succeeded", transactionId: "pay_1N3xDR", }, { amount: 299, customer: "Isabella Nguyen", email: "isabella@example.com", fee: 8.97, id: "2", method: "Visa •••• 1234", status: "succeeded", transactionId: "pay_1N3x9M", }, { amount: 39, customer: "Jackson Lee", email: "jackson@example.com", fee: 1.43, id: "3", method: "Mastercard •••• 5555", status: "processing", transactionId: "pay_1N3x8L", }, { amount: 150, customer: "Liam Johnson", email: "liam@example.com", fee: 4.5, id: "4", method: "Mastercard •••• 6789", status: "refunded", transactionId: "pay_1N3xCQ", }, { amount: 1999, customer: "Olivia Martin", email: "olivia@example.com", fee: 59.97, id: "5", method: "Visa •••• 4242", status: "succeeded", transactionId: "pay_1N3x7K", }, { amount: 99, customer: "William Kim", email: "will@example.com", fee: 3.17, id: "6", method: "Amex •••• 3782", status: "failed", transactionId: "pay_1N3xAN", }, ]; const statusColor: Record = { failed: "danger", processing: "warning", refunded: "default", succeeded: "success", }; const formatCurrency = (value: number) => value.toLocaleString("en-US", {currency: "USD", style: "currency"}); const columns: DataGridColumn[] = [ { accessorKey: "customer", allowsSorting: true, cell: (item) => (
{item.customer} {item.email}
), header: "Customer", id: "customer", isRowHeader: true, }, { accessorKey: "transactionId", cell: (item) => {item.transactionId}, header: "Transaction ID", id: "transactionId", }, { accessorKey: "status", cell: (item) => ( {item.status.charAt(0).toUpperCase() + item.status.slice(1)} ), header: "Status", id: "status", }, { accessorKey: "amount", align: "end", allowsSorting: true, cell: (item) => {formatCurrency(item.amount)}, header: "Amount", id: "amount", }, { accessorKey: "fee", align: "end", cell: (item) => {formatCurrency(item.fee)}, header: "Fee", id: "fee", }, {accessorKey: "method", header: "Payment Method", id: "method"}, { cell: () => ( ), header: "", id: "actions", }, ]; export function Default() { return ( item.id} selectionMode="multiple" /> ); } ``` ### Anatomy Unlike most DarkCode UI components, `DataGrid` is not a compound component — it has no `DataGrid.*` sub-parts. Everything is configured through props and column definitions. ```tsx import { DataGrid } from '@darkcode-ui/react'; item.id} /> ``` ### Column Definitions Columns are defined as an array of `DataGridColumn` objects. Each column has an `id`, a `header`, and either an `accessorKey` (to read a value from the row object) or a custom `cell` renderer. ```tsx import type { DataGridColumn } from '@darkcode-ui/react'; interface Payment { id: string; customer: string; amount: number; status: 'succeeded' | 'failed'; } const columns: DataGridColumn[] = [ { id: 'customer', header: 'Customer', accessorKey: 'customer', isRowHeader: true, allowsSorting: true, }, { id: 'amount', header: 'Amount', accessorKey: 'amount', align: 'end', cell: (item) => `$${item.amount.toFixed(2)}`, }, { id: 'status', header: 'Status', accessorKey: 'status', }, ]; ``` The `cell` function receives the full row item and the column definition and can return any `ReactNode`. The `header` property accepts a string, a `ReactNode`, or a render function that receives `{ sortDirection }` for sortable columns. ### Sorting Mark columns as sortable with `allowsSorting: true`. In uncontrolled mode, the DataGrid sorts data client-side using locale-aware comparison (or a custom `sortFn` on the column). For server-side sorting, pass a controlled `sortDescriptor` and handle `onSortChange`. ```tsx // Uncontrolled (client-side) // Controlled (server-side) ``` ### Row Selection Enable row selection with `selectionMode` and `showSelectionCheckboxes`. Both `"single"` and `"multiple"` modes are supported, with controlled (`selectedKeys` + `onSelectionChange`) or uncontrolled (`defaultSelectedKeys`) state. ### Pinned Columns Pin columns to the start or end edge with `pinned: "start"` or `pinned: "end"` so they stay visible during horizontal scroll. Pinned columns must have a numeric `width` or `minWidth`. A separator appears on the boundary column whenever the pinned group detaches from the scrollable content. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Chip, DataGrid, Link} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; interface Company { id: string; name: string; dotColor: string; categories: string[]; linkedin: string; lastInteraction: string; strength: "none" | "weak" | "very-weak" | "very-strong"; followers: number; twitter: string; } const companies: Company[] = [ { categories: ["B2C", "Consumer Discretionary", "E-commerce"], dotColor: "bg-warning", followers: 198135, id: "1", lastInteraction: "No contact", linkedin: "lvmh", name: "LVMH", strength: "none", twitter: "LVMH", }, { categories: ["B2C", "E-commerce"], dotColor: "bg-success", followers: 10140332, id: "2", lastInteraction: "No contact", linkedin: "disneymusicgroup", name: "Disney", strength: "none", twitter: "Disney", }, { categories: ["B2C", "Finance", "Financial Services"], dotColor: "bg-accent", followers: 969425, id: "3", lastInteraction: "No contact", linkedin: "paypal", name: "PayPal", strength: "none", twitter: "PayPal", }, { categories: ["B2C", "E-commerce", "Food"], dotColor: "bg-success", followers: 3200, id: "4", lastInteraction: "about 2 hours ago", linkedin: "sweetgreen", name: "sweetgreen", strength: "very-strong", twitter: "sweetgreen", }, { categories: ["Automation", "B2B", "Enterprise"], dotColor: "bg-accent", followers: 1340, id: "5", lastInteraction: "about 3 hours ago", linkedin: "attio", name: "Attio", strength: "very-weak", twitter: "attio", }, { categories: ["B2B", "B2C", "Broadcasting"], dotColor: "bg-accent", followers: 28946065, id: "6", lastInteraction: "9 days ago", linkedin: "google", name: "Google", strength: "weak", twitter: "Google", }, ]; const strengthDisplay: Record = { none: {className: "text-muted", label: "No communication"}, "very-strong": {className: "text-success", label: "Very strong"}, "very-weak": {className: "text-danger", label: "Very weak"}, weak: {className: "text-warning", label: "Weak"}, }; const chipColors = ["warning", "danger", "success", "accent"] as const; const columns: DataGridColumn[] = [ { accessorKey: "name", cell: (item) => ( {item.name} ), header: "Company", id: "name", isRowHeader: true, minWidth: 180, pinned: "start", }, { cell: (item) => ( {item.categories.slice(0, 3).map((category) => ( {category.length > 14 ? `${category.slice(0, 12)}…` : category} ))} ), header: "Categories", id: "categories", minWidth: 320, }, { accessorKey: "linkedin", cell: (item) => ( {item.linkedin} ), header: "LinkedIn", id: "linkedin", minWidth: 180, }, { accessorKey: "lastInteraction", cell: (item) => {item.lastInteraction}, header: "Last interaction", id: "lastInteraction", minWidth: 170, }, { cell: (item) => ( {strengthDisplay[item.strength].label} ), header: "Connection strength", id: "strength", minWidth: 190, }, { accessorKey: "followers", align: "end", cell: (item) => {item.followers.toLocaleString("en-US")}, header: "Twitter followers", id: "followers", minWidth: 150, }, { accessorKey: "twitter", cell: (item) => {item.twitter}, header: "Twitter", id: "twitter", minWidth: 140, pinned: "end", }, ]; export function PinnedColumns() { return ( item.id} selectionMode="multiple" /> ); } ``` ### Drag and Drop Enable row reorder with the `onReorder` callback. The DataGrid renders built-in drag handles with keyboard support (Enter to grab, arrows to move, Enter to drop) and fires the callback with the reordered data array. For advanced scenarios, pass `dragAndDropHooks` from React Aria's `useDragAndDrop` instead — it takes precedence over `onReorder`. With expandable rows enabled, the built-in reorder applies to top-level rows only. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Chip, DataGrid} from "@darkcode-ui/react"; import {useState} from "react"; interface Task { id: string; title: string; priority: "high" | "medium" | "low"; assignee: string; status: "todo" | "in-progress" | "done"; } const initialTasks: Task[] = [ { assignee: "Olivia", id: "1", priority: "high", status: "in-progress", title: "Design system audit", }, {assignee: "Jackson", id: "2", priority: "high", status: "todo", title: "API rate limiting"}, { assignee: "Isabella", id: "3", priority: "medium", status: "in-progress", title: "Onboarding flow redesign", }, { assignee: "William", id: "4", priority: "high", status: "todo", title: "Database migration script", }, { assignee: "Sofia", id: "5", priority: "low", status: "done", title: "Unit test coverage report", }, { assignee: "Liam", id: "6", priority: "medium", status: "todo", title: "Performance profiling", }, ]; const priorityColor: Record = { high: "danger", low: "default", medium: "warning", }; const statusDisplay: Record< Task["status"], {color: "warning" | "success" | "default"; label: string} > = { done: {color: "success", label: "Done"}, "in-progress": {color: "warning", label: "In Progress"}, todo: {color: "default", label: "To Do"}, }; const columns: DataGridColumn[] = [ {accessorKey: "title", header: "Task", id: "title", isRowHeader: true}, { accessorKey: "priority", cell: (item) => ( {item.priority.charAt(0).toUpperCase() + item.priority.slice(1)} ), header: "Priority", id: "priority", }, {accessorKey: "assignee", header: "Assignee", id: "assignee"}, { accessorKey: "status", cell: (item) => ( {statusDisplay[item.status].label} ), header: "Status", id: "status", }, ]; export function DragAndDrop() { const [tasks, setTasks] = useState(initialTasks); return (

Drag rows to reorder. Use keyboard (Enter to grab, arrows to move, Enter to drop).

item.id} onReorder={(event) => setTasks(event.reorderedData)} />
); } ``` ### Expandable Rows Render hierarchical data by providing a `getChildren` function. The DataGrid recursively renders child rows, auto-generates a chevron toggle in the `treeColumn`, and indents each nested level by `treeIndent` pixels (default `20`, set `0` to disable). The `treeColumn` prop specifies which column displays the chevron. If omitted, it defaults to the first `isRowHeader` column (or the first column). Use `defaultExpandedKeys` for uncontrolled expansion, or pair `expandedKeys` with `onExpandedChange` for controlled behavior. With client-side sorting, each sibling group is sorted independently. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Chip, DataGrid} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; interface FileRow { id: string; name: string; type: "Folder" | "Text"; size: string; modified: string; children?: FileRow[]; } const files: FileRow[] = [ { children: [ { children: [ {id: "3", modified: "Jul 10, 2025", name: "Weekly Report.md", size: "8 KB", type: "Text"}, {id: "4", modified: "Aug 20, 2025", name: "Budget.md", size: "6 KB", type: "Text"}, ], id: "2", modified: "Aug 2, 2025", name: "Project Alpha", size: "2 items", type: "Folder", }, {id: "8", modified: "Sep 14, 2025", name: "Meeting Notes.md", size: "12 KB", type: "Text"}, ], id: "1", modified: "Oct 20, 2025", name: "Documents", size: "3 items", type: "Folder", }, { children: [ {id: "6", modified: "Jan 23, 2026", name: "Image 1.png", size: "1.2 MB", type: "Text"}, {id: "7", modified: "Feb 3, 2026", name: "Image 2.png", size: "2.1 MB", type: "Text"}, ], id: "5", modified: "Feb 3, 2026", name: "Photos", size: "2 items", type: "Folder", }, {id: "9", modified: "Mar 1, 2026", name: "readme.txt", size: "4 KB", type: "Text"}, ]; const columns: DataGridColumn[] = [ { accessorKey: "name", cell: (item) => ( {item.name} ), header: "Name", id: "name", isRowHeader: true, }, { accessorKey: "type", cell: (item) => ( {item.type} ), header: "Type", id: "type", }, {accessorKey: "size", align: "end", header: "Size", id: "size"}, {accessorKey: "modified", header: "Date Modified", id: "modified"}, ]; export function ExpandableRows() { return ( item.children} getRowId={(item) => item.id} treeColumn="name" /> ); } ``` ### Editable Cells Use the `cell` render function to embed any interactive component — selects, number steppers, switches, text fields, and more. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Chip, DataGrid, ListBox, NumberField, Select, Switch} from "@darkcode-ui/react"; import {useState} from "react"; interface Feature { id: string; name: string; priority: "high" | "medium" | "low"; points: number; enabled: boolean; } const initialFeatures: Feature[] = [ {enabled: true, id: "1", name: "Dark mode", points: 8, priority: "high"}, {enabled: true, id: "2", name: "CSV export", points: 3, priority: "medium"}, {enabled: false, id: "3", name: "Keyboard shortcuts", points: 5, priority: "low"}, {enabled: true, id: "4", name: "Two-factor auth", points: 13, priority: "high"}, {enabled: false, id: "5", name: "Bulk import", points: 8, priority: "medium"}, {enabled: true, id: "6", name: "Audit log", points: 5, priority: "high"}, ]; const priorityColor: Record = { high: "danger", low: "default", medium: "warning", }; export function EditableCells() { const [features, setFeatures] = useState(initialFeatures); const updateFeature = (id: string, patch: Partial) => { setFeatures((current) => current.map((feature) => (feature.id === id ? {...feature, ...patch} : feature)), ); }; const columns: DataGridColumn[] = [ { accessorKey: "name", cell: (item) => {item.name}, header: "Feature", id: "name", isRowHeader: true, }, { cell: (item) => ( ), header: "Priority", id: "priority", }, { align: "center", cell: (item) => ( updateFeature(item.id, {points: value})} > ), header: "Story Points", id: "points", }, { cell: (item) => ( updateFeature(item.id, {enabled: isSelected})} > ), header: "Enabled", id: "enabled", }, ]; const enabledCount = features.filter((feature) => feature.enabled).length; const totalPoints = features.reduce((sum, feature) => sum + feature.points, 0); return (
item.id} />

{enabledCount} of {features.length} enabled · Total effort: {totalPoints} pts

); } ``` ### Empty State Provide a `renderEmptyState` function to display a custom empty state when `data` is empty. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Button, DataGrid, EmptyState} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; interface Project { id: string; name: string; owner: string; files: number; updated: string; } const columns: DataGridColumn[] = [ {accessorKey: "name", header: "Project", id: "name", isRowHeader: true}, {accessorKey: "owner", header: "Owner", id: "owner"}, {accessorKey: "files", align: "end", header: "Files", id: "files"}, {accessorKey: "updated", header: "Last Updated", id: "updated"}, ]; export function EmptyStateDemo() { return ( item.id} renderEmptyState={() => ( No Projects Yet Get started by creating your first project. You can always import existing projects later. )} /> ); } ``` ### Async Loading Use `onLoadMore`, `isLoadingMore`, and `loadMoreContent` to implement infinite scroll loading. The DataGrid renders a sentinel row that triggers `onLoadMore` when it scrolls into view. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Chip, DataGrid, Spinner} from "@darkcode-ui/react"; import {useState} from "react"; interface Invoice { id: string; invoice: string; client: string; amount: number; status: "paid" | "pending" | "overdue"; issued: string; } const clients = [ "Cyberdyne Systems", "Soylent Corp", "LexCorp", "Pied Piper", "Massive Dynamic", "Umbrella LLC", "Hooli", "Initech", ]; const statuses: Invoice["status"][] = ["overdue", "paid", "paid", "pending", "paid"]; function generateInvoices(start: number, count: number): Invoice[] { return Array.from({length: count}, (_, index) => { const number = start + index; return { amount: ((number * 3767) % 9500) + 300, client: clients[number % clients.length] ?? "Acme Corp", id: String(number), invoice: `INV-${1000 + number}`, issued: new Date(2025, number % 12, (number % 27) + 1).toLocaleDateString("en-US", { day: "numeric", month: "short", year: "numeric", }), status: statuses[number % statuses.length] ?? "paid", }; }); } const statusDisplay: Record< Invoice["status"], {color: "success" | "warning" | "danger"; label: string} > = { overdue: {color: "danger", label: "Overdue"}, paid: {color: "success", label: "Paid"}, pending: {color: "warning", label: "Pending"}, }; const columns: DataGridColumn[] = [ { accessorKey: "invoice", cell: (item) => {item.invoice}, header: "Invoice", id: "invoice", isRowHeader: true, }, { accessorKey: "client", cell: (item) => {item.client}, header: "Client", id: "client", }, { accessorKey: "amount", align: "end", cell: (item) => ${item.amount.toLocaleString("en-US")}, header: "Amount", id: "amount", }, { accessorKey: "status", cell: (item) => ( {statusDisplay[item.status].label} ), header: "Status", id: "status", }, { accessorKey: "issued", cell: (item) => {item.issued}, header: "Issued", id: "issued", }, ]; const TOTAL = 50; export function AsyncLoading() { const [items, setItems] = useState(() => generateInvoices(0, 8)); const [isLoading, setIsLoading] = useState(false); const hasMore = items.length < TOTAL; const handleLoadMore = () => { if (isLoading) return; setIsLoading(true); setTimeout(() => { setItems((current) => [ ...current, ...generateInvoices(current.length, Math.min(8, TOTAL - current.length)), ]); setIsLoading(false); }, 900); }; return (

Invoices

{items.length} / {TOTAL}
item.id} isLoadingMore={isLoading} loadMoreContent={} scrollContainerClassName="max-h-[400px] overflow-y-auto" onLoadMore={hasMore ? handleLoadMore : undefined} />
); } ``` ### Column Resizing Enable column resizing with `allowsColumnResize` on the DataGrid and `allowsResizing` on individual columns. Columns should have `minWidth` set. Track widths with `onColumnResize` and `onColumnResizeEnd`. ### Virtualization Enable row virtualization for large datasets (1,000+ rows) with the `virtualized` prop. Only visible rows are rendered to the DOM. Set `rowHeight` and `headingHeight` to match your row content, and constrain the height via `contentClassName`. ```tsx "use client"; import type {DataGridColumn, DataGridSelection} from "@darkcode-ui/react"; import {Chip, DataGrid, SearchField, ToggleButton, ToggleButtonGroup} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; import {useState} from "react"; interface Product { id: string; name: string; sku: string; status: "in-stock" | "low-stock" | "out-of-stock"; stock: number; price: number; rating: number; sales: number; } const brands = ["ZenCore", "EchoLink", "TechFlow", "PrimeByte", "PixelForge"]; const lines = ["Turbo", "Advanced", "Deluxe", "Elite", "Slim", "Ultra"]; const kinds = ["Sensor", "Mouse", "Drive", "Tablet", "Cable", "Headphones", "Webcam"]; function generateProducts(count: number): Product[] { return Array.from({length: count}, (_, index) => { const stock = (index * 37) % 180 === 0 ? 0 : (index * 37) % 180; const status: Product["status"] = stock === 0 ? "out-of-stock" : stock < 6 ? "low-stock" : "in-stock"; return { id: String(index + 1), name: `${brands[index % brands.length]} ${lines[index % lines.length]} ${ kinds[index % kinds.length] }`, price: ((index * 977) % 1500) + 300, rating: ((index * 13) % 21) / 4 + 0.5, sales: (index * 179) % 800, sku: `SKU-${3000 + index}`, status, stock, }; }); } const products = generateProducts(1000); const statusDisplay: Record< Product["status"], {color: "success" | "warning" | "danger"; icon: string; label: string} > = { "in-stock": {color: "success", icon: "gravity-ui:circle-check-fill", label: "In Stock"}, "low-stock": {color: "warning", icon: "gravity-ui:clock-fill", label: "Low Stock"}, "out-of-stock": {color: "danger", icon: "gravity-ui:circle-xmark-fill", label: "Out of Stock"}, }; function StarRating({value}: {value: number}) { return ( {Array.from({length: 5}, (_, index) => ( ))} ); } const columns: DataGridColumn[] = [ { accessorKey: "name", cell: (item) => ( {item.name} ), header: "Product", id: "name", isRowHeader: true, width: 280, }, { accessorKey: "sku", cell: (item) => {item.sku}, header: "SKU", id: "sku", width: 120, }, { accessorKey: "status", cell: (item) => ( {statusDisplay[item.status].label} ), header: "Status", id: "status", width: 150, }, {accessorKey: "stock", align: "end", header: "Stock", id: "stock", width: 90}, { accessorKey: "price", align: "end", cell: (item) => ${item.price.toLocaleString("en-US")}, header: "Price", id: "price", width: 100, }, { accessorKey: "rating", cell: (item) => , header: "Rating", id: "rating", width: 130, }, {accessorKey: "sales", align: "end", header: "Sales", id: "sales", width: 90}, ]; export function Virtualized() { const [filter, setFilter] = useState(new Set(["all"])); const [query, setQuery] = useState(""); const activeFilter = filter === "all" ? "all" : ([...filter][0] ?? "all"); const filteredProducts = products.filter((product) => { if (activeFilter !== "all" && product.status !== activeFilter) return false; if (query && !product.name.toLowerCase().includes(query.toLowerCase())) return false; return true; }); return (
{ setFilter((keys as Set).size === 0 ? new Set(["all"]) : keys); }} > All In Stock Out of Stock Low Stock
item.id} headingHeight={37} rowHeight={58} selectionMode="multiple" />
); } ``` ### Bulk Actions Combine row selection with an `ActionBar` to provide bulk operations like export, archive, or delete. ```tsx "use client"; import type {DataGridColumn, DataGridSelection} from "@darkcode-ui/react"; import {ActionBar, Avatar, Chip, DataGrid} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; import {useState} from "react"; interface Employee { id: string; name: string; email: string; avatar: string; department: string; status: "active" | "pending" | "inactive"; joined: string; } const employees: Employee[] = [ { avatar: "https://darkcode-ui-assets.darkcode.dev/avatars/red.jpg", department: "Support", email: "amara.okafor@company.com", id: "1", joined: "Jun 27, 2024", name: "Amara Okafor", status: "pending", }, { avatar: "https://darkcode-ui-assets.darkcode.dev/avatars/green.jpg", department: "HR", email: "elena.rodriguez@company.com", id: "2", joined: "Jan 28, 2024", name: "Elena Rodriguez", status: "active", }, { avatar: "https://darkcode-ui-assets.darkcode.dev/avatars/blue.jpg", department: "Finance", email: "james.o.brien@company.com", id: "3", joined: "Apr 14, 2024", name: "James O'Brien", status: "active", }, { avatar: "https://darkcode-ui-assets.darkcode.dev/avatars/purple.jpg", department: "Engineering", email: "luca.bianchi@company.com", id: "4", joined: "Jul 25, 2024", name: "Luca Bianchi", status: "active", }, { avatar: "https://darkcode-ui-assets.darkcode.dev/avatars/orange.jpg", department: "Design", email: "marcus.chen@company.com", id: "5", joined: "Feb 3, 2024", name: "Marcus Chen", status: "pending", }, { avatar: "https://darkcode-ui-assets.darkcode.dev/avatars/black.jpg", department: "Product", email: "yuki.tanaka@company.com", id: "6", joined: "May 8, 2024", name: "Yuki Tanaka", status: "inactive", }, ]; const statusDisplay: Record< Employee["status"], {color: "success" | "warning" | "danger"; label: string} > = { active: {color: "success", label: "Active"}, inactive: {color: "danger", label: "Inactive"}, pending: {color: "warning", label: "Pending"}, }; const columns: DataGridColumn[] = [ { accessorKey: "name", allowsSorting: true, cell: (item) => ( {item.name.charAt(0)} {item.name} {item.email} ), header: "Employee", id: "name", isRowHeader: true, }, {accessorKey: "department", header: "Department", id: "department"}, { accessorKey: "status", cell: (item) => ( {statusDisplay[item.status].label} ), header: "Status", id: "status", }, {accessorKey: "joined", header: "Joined", id: "joined"}, ]; export function BulkActions() { const [selectedKeys, setSelectedKeys] = useState(new Set()); const selectedCount = selectedKeys === "all" ? employees.length : selectedKeys.size; return (
item.id} selectedKeys={selectedKeys} selectionMode="multiple" onSelectionChange={setSelectedKeys} /> 0} onClose={() => setSelectedKeys(new Set())}> }> Export }> Archive } variant="danger-soft" > Delete
); } ``` ### Users A minimal user directory using the `"secondary"` variant with `accessorKey`-only columns and a row action link — no selection, no sorting. ```tsx "use client"; import type {DataGridColumn} from "@darkcode-ui/react"; import {Button, DataGrid, Link} from "@darkcode-ui/react"; interface User { id: string; name: string; title: string; email: string; role: string; } const users: User[] = [ { email: "lindsay.walton@example.com", id: "1", name: "Lindsay Walton", role: "Member", title: "Front-end Developer", }, { email: "courtney.henry@example.com", id: "2", name: "Courtney Henry", role: "Admin", title: "Designer", }, { email: "tom.cook@example.com", id: "3", name: "Tom Cook", role: "Member", title: "Director of Product", }, { email: "whitney.francis@example.com", id: "4", name: "Whitney Francis", role: "Admin", title: "Copywriter", }, { email: "leonard.krasner@example.com", id: "5", name: "Leonard Krasner", role: "Owner", title: "Senior Designer", }, { email: "floyd.miles@example.com", id: "6", name: "Floyd Miles", role: "Member", title: "Principal Designer", }, ]; const columns: DataGridColumn[] = [ { accessorKey: "name", cell: (item) => {item.name}, header: "Name", id: "name", isRowHeader: true, }, {accessorKey: "title", header: "Title", id: "title"}, {accessorKey: "email", header: "Email", id: "email"}, {accessorKey: "role", header: "Role", id: "role"}, { align: "end", cell: (item) => ( Edit ), header: "", id: "edit", }, ]; export function Users() { return (

Users

A list of all the users in your account including their name, title, email and role.

item.id} variant="secondary" />
); } ``` ### Team Members A full-featured HR table with controlled sorting, multi-selection, pinned columns, column resizing, column visibility toggling, and search. ```tsx "use client"; import type {DataGridColumn, DataGridSelection, DataGridSortDescriptor} from "@darkcode-ui/react"; import {Avatar, Button, Chip, DataGrid, Dropdown, Label, SearchField} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; import {useState} from "react"; interface Member { id: string; workerId: string; name: string; email: string; avatar: string; country: string; flag: string; role: string; workerType: "Employee" | "Contractor"; status: "active" | "inactive" | "vacation"; startDate: string; } const seed: [ string, string, string, string, string, Member["workerType"], Member["status"], string, ][] = [ [ "Aaron Carter", "red", "Canada", "🇨🇦", "Software Engineer", "Contractor", "inactive", "Sep 16, 2025", ], [ "Alice Johnson", "green", "South Korea", "🇰🇷", "Network Administrator", "Contractor", "active", "May 5, 2025", ], [ "Bella Brown", "blue", "United Kingdom", "🇬🇧", "Technical Support Specialist", "Employee", "active", "Jul 12, 2025", ], [ "Carlos Mendez", "purple", "United States", "🇺🇸", "Operations Coordinator", "Contractor", "vacation", "Apr 14, 2025", ], ["Daniela Rossi", "orange", "Brazil", "🇧🇷", "Accountant", "Employee", "vacation", "Jan 14, 2025"], [ "Emi Sato", "black", "Japan", "🇯🇵", "Graphic Designer", "Contractor", "inactive", "Apr 11, 2026", ], [ "Farah Khan", "red", "India", "🇮🇳", "Technical Support Specialist", "Employee", "active", "Apr 24, 2026", ], ["Gustav Berg", "green", "Sweden", "🇸🇪", "Product Manager", "Employee", "active", "Feb 2, 2026"], ]; const members: Member[] = seed.map( ([name, color, country, flag, role, workerType, status, startDate], index) => ({ avatar: `https://darkcode-ui-assets.darkcode.dev/avatars/${color}.jpg`, country, email: `${name.toLowerCase().replace(/[^a-z]+/g, ".")}@example.com`, flag, id: String(index + 1), name, role, startDate, status, workerId: `WRK-${9100000 + index * 37231}`, workerType, }), ); const statusDisplay: Record< Member["status"], {color: "success" | "warning" | "danger"; label: string} > = { active: {color: "success", label: "Active"}, inactive: {color: "danger", label: "Inactive"}, vacation: {color: "warning", label: "Vacation"}, }; export function TeamMembers() { const [selectedKeys, setSelectedKeys] = useState(new Set()); const [sortDescriptor, setSortDescriptor] = useState({ column: "name", direction: "ascending", }); const [query, setQuery] = useState(""); const [visibleColumns, setVisibleColumns] = useState( new Set(["workerId", "name", "country", "role", "workerType", "status", "startDate"]), ); const allColumns: DataGridColumn[] = [ { accessorKey: "workerId", cell: (item) => {item.workerId}, header: "Worker ID", id: "workerId", minWidth: 140, }, { accessorKey: "name", allowsResizing: true, allowsSorting: true, cell: (item) => ( {item.name.charAt(0)} {item.name} {item.email} ), header: "Member", id: "name", isRowHeader: true, minWidth: 220, pinned: "start", width: 260, }, { accessorKey: "country", allowsResizing: true, allowsSorting: true, cell: (item) => ( {item.flag} {item.country} ), header: "Country", id: "country", minWidth: 150, }, {accessorKey: "role", allowsResizing: true, header: "Role", id: "role", minWidth: 200}, { accessorKey: "workerType", allowsSorting: true, header: "Worker Type", id: "workerType", minWidth: 130, }, { accessorKey: "status", cell: (item) => ( {statusDisplay[item.status].label} ), header: "Status", id: "status", minWidth: 110, }, { accessorKey: "startDate", cell: (item) => ( {item.startDate} ), header: "Start Date", id: "startDate", minWidth: 170, }, ]; const columns = allColumns.filter( (column) => visibleColumns === "all" || visibleColumns.has(column.id), ); const rows = members .filter((member) => member.name.toLowerCase().includes(query.toLowerCase())) .sort((a, b) => { const key = sortDescriptor.column as keyof Member; const result = String(a[key]).localeCompare(String(b[key])); return sortDescriptor.direction === "descending" ? -result : result; }); return (
{allColumns.map((column) => ( ))}
item.id} selectedKeys={selectedKeys} selectionMode="multiple" sortDescriptor={sortDescriptor} onSelectionChange={setSelectedKeys} onSortChange={setSortDescriptor} />
); } ``` ### Servers A server monitoring dashboard with controlled sorting, selection, search, and rich cell renderers including sparkline charts and circular progress indicators. ```tsx "use client"; import type {DataGridColumn, DataGridSelection, DataGridSortDescriptor} from "@darkcode-ui/react"; import {Button, Chip, DataGrid, ProgressCircle, SearchField} from "@darkcode-ui/react"; import {Icon} from "@iconify/react"; import {useState} from "react"; interface Server { id: string; cluster: string; instances: number; status: "active" | "inactive"; region: string; capacity: number; cost: number; requests: number[]; } function generateSparkline(seed: number): number[] { return Array.from({length: 16}, (_, index) => 30 + ((seed * (index + 3) * 7919) % 40)); } const servers: Server[] = [ { capacity: 20.01, cluster: "#4586930", cost: 830.04, id: "1", instances: 25, region: "US-West 1", requests: generateSparkline(1), status: "active", }, { capacity: 7.36, cluster: "#4586931", cost: 752.72, id: "2", instances: 13, region: "US-East 2", requests: generateSparkline(2), status: "active", }, { capacity: 48.25, cluster: "#4586932", cost: 517.6, id: "3", instances: 27, region: "EU-Central 1", requests: generateSparkline(3), status: "inactive", }, { capacity: 21.15, cluster: "#4586933", cost: 544.93, id: "4", instances: 28, region: "AP-South 1", requests: generateSparkline(4), status: "inactive", }, { capacity: 73.12, cluster: "#4586934", cost: 412.71, id: "5", instances: 6, region: "US-West 2", requests: generateSparkline(5), status: "active", }, { capacity: 72.65, cluster: "#4586935", cost: 354.06, id: "6", instances: 19, region: "EU-West 1", requests: generateSparkline(6), status: "active", }, { capacity: 15.19, cluster: "#4586936", cost: 775.45, id: "7", instances: 27, region: "CA-Central 1", requests: generateSparkline(7), status: "active", }, { capacity: 11.35, cluster: "#4586937", cost: 146.05, id: "8", instances: 27, region: "AP-Northeast 1", requests: generateSparkline(8), status: "active", }, ]; function Sparkline({values}: {values: number[]}) { const max = Math.max(...values); const min = Math.min(...values); const range = max - min || 1; const points = values .map((value, index) => { const x = (index / (values.length - 1)) * 96; const y = 26 - ((value - min) / range) * 22; return `${x.toFixed(1)},${y.toFixed(1)}`; }) .join(" "); return ( ); } const capacityColor = (value: number): "accent" | "warning" | "danger" => value >= 90 ? "danger" : value >= 70 ? "warning" : "accent"; const formatCurrency = (value: number) => value.toLocaleString("en-US", {currency: "USD", style: "currency"}); const columns: DataGridColumn[] = [ { accessorKey: "cluster", allowsSorting: true, cell: (item) => ( {item.cluster} ), header: "Cluster ID", id: "cluster", isRowHeader: true, }, { accessorKey: "instances", align: "center", allowsSorting: true, header: "Instances", id: "instances", }, { accessorKey: "status", cell: (item) => ( {item.status === "active" ? "Active" : "Inactive"} ), header: "Status", id: "status", }, {accessorKey: "region", header: "Region", id: "region"}, { accessorKey: "capacity", cell: (item) => ( = 70 ? "text-warning" : undefined}> {item.capacity.toFixed(2)}% ), header: "Capacity", id: "capacity", }, { accessorKey: "cost", align: "end", allowsSorting: true, cell: (item) => {formatCurrency(item.cost)}, header: "Cost", id: "cost", }, { cell: (item) => , header: "Requests", id: "requests", }, ]; export function Servers() { const [selectedKeys, setSelectedKeys] = useState(new Set()); const [sortDescriptor, setSortDescriptor] = useState({ column: "cluster", direction: "ascending", }); const [query, setQuery] = useState(""); const rows = servers .filter( (server) => server.cluster.includes(query) || server.region.toLowerCase().includes(query.toLowerCase()), ) .sort((a, b) => { const key = sortDescriptor.column as keyof Server; const av = a[key]; const bv = b[key]; const result = typeof av === "number" && typeof bv === "number" ? av - bv : String(av).localeCompare(String(bv)); return sortDescriptor.direction === "descending" ? -result : result; }); return (

Servers

{rows.length}
item.id} selectedKeys={selectedKeys} selectionMode="multiple" sortDescriptor={sortDescriptor} onSelectionChange={setSelectedKeys} onSortChange={setSortDescriptor} />
); } ``` ## Styling ### CSS Classes - `.data-grid` — Root wrapper. Defines the `--data-grid-*` custom properties. - `.data-grid__selection-column` / `.data-grid__selection-cell` — Narrow column for the select-all and row checkboxes. Width from `--data-grid-selection-column-width`. - `.data-grid__drag-handle-column` / `.data-grid__drag-handle-cell` — Narrow column for the drag handles. Width from `--data-grid-drag-handle-column-width`. - `.data-grid__drag-handle` — The grip button inside each row. `cursor: grab`, `grabbing` while pressed. - `.data-grid__sort-icon` — Chevron next to sortable column headers. Rotates 180° when descending. - `.data-grid__empty-state` — Centered container for the empty state message. - `.data-grid__tree-cell` — Flex wrapper inside the `treeColumn` cell holding the chevron toggle and cell content. Per-level indentation via `--data-grid-tree-indent`. - `.data-grid__tree-toggle` / `.data-grid__tree-toggle-icon` — Expand/collapse chevron button and icon. The icon rotates 90° when expanded via `[data-expanded]`. - `.data-grid__tree-toggle-spacer` — Invisible placeholder for leaf rows so content aligns with siblings that have a chevron. ### Data Attributes - `[data-align="center" | "end"]` — Set on header and body cells from the column `align` option. - `[data-vertical-align="top" | "middle" | "bottom"]` — Set on the root from the `verticalAlign` prop. - `[data-pinned="start" | "end"]` — Sticky pinned cells. - `[data-pinned-edge]` — Boundary column of a pinned group; shows the separator. - `[data-pinned-start-detached]` / `[data-pinned-end-detached]` — Set on the root while the content is scrolled away from the pinned edge. ### CSS Variables - `--data-grid-selection-column-width` — Width of the selection checkbox column (default `40px`). - `--data-grid-drag-handle-column-width` — Width of the drag handle column (default `32px`). - `--data-grid-tree-toggle-size` — Size of the expand/collapse chevron button and its leaf-row spacer (default `24px`). - `--data-grid-tree-gap` — Gap between the chevron/spacer and the cell content (default `4px`). - `--data-grid-tree-indent` — Indentation per nested level (default `20px`, set from the `treeIndent` prop). ## API Reference ### DataGrid Accepts a generic type parameter `T` for the row data shape. | Prop | Type | Default | Description | |------|------|---------|-------------| | `data` | `T[]` | - | Row data array | | `columns` | `DataGridColumn[]` | - | Column definitions | | `getRowId` | `(item: T) => Key` | - | Extracts a unique key from each row item | | `aria-label` | `string` | - | Accessible label for the table. Required | | `variant` | `'primary' \| 'secondary'` | `'primary'` | Visual variant passed to the underlying Table | | `className` | `string` | - | Additional className for the root wrapper | | `contentClassName` | `string` | - | Additional className for the inner `` element | | `scrollContainerClassName` | `string` | - | Additional className for the scroll container | | `verticalAlign` | `'top' \| 'middle' \| 'bottom'` | `'middle'` | Vertical alignment of cell content | | `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | Row selection mode | | `selectedKeys` | `DataGridSelection` | - | Controlled selected row keys | | `defaultSelectedKeys` | `DataGridSelection` | - | Default selected row keys (uncontrolled) | | `onSelectionChange` | `(keys: DataGridSelection) => void` | - | Callback when selection changes | | `selectionBehavior` | `'toggle' \| 'replace'` | `'toggle'` | Selection interaction model | | `showSelectionCheckboxes` | `boolean` | `false` | Auto-prepend a checkbox column for selection | | `disabledKeys` | `Iterable` | - | Keys of rows that should be disabled | | `sortDescriptor` | `DataGridSortDescriptor` | - | Controlled sort descriptor | | `defaultSortDescriptor` | `DataGridSortDescriptor` | - | Default sort descriptor (uncontrolled client-side sorting) | | `onSortChange` | `(descriptor: DataGridSortDescriptor) => void` | - | Callback when sort changes. Fires in both modes | | `allowsColumnResize` | `boolean` | `false` | Enable column resizing on columns that opt in | | `onColumnResize` | `(widths: Map) => void` | - | Callback during column resize | | `onColumnResizeEnd` | `(widths: Map) => void` | - | Callback when resize ends | | `onReorder` | `(event: DataGridReorderEvent) => void` | - | Enables built-in drag-and-drop row reorder | | `dragAndDropHooks` | `DataGridDragAndDropHooks` | - | Advanced React Aria drag-and-drop hooks. Overrides `onReorder` | | `onRowAction` | `(key: Key) => void` | - | Callback when a row is actioned | | `renderEmptyState` | `() => ReactNode` | - | Render function for the empty state | | `onLoadMore` | `() => void` | - | Callback when the load-more sentinel scrolls into view | | `isLoadingMore` | `boolean` | `false` | Whether more data is currently being fetched | | `loadMoreContent` | `ReactNode` | - | Content inside the load-more sentinel row | | `virtualized` | `boolean` | `false` | Enable row virtualization for large datasets | | `rowHeight` | `number` | `42` | Fixed row height in pixels (virtualized mode) | | `headingHeight` | `number` | `36` | Header row height in pixels (virtualized mode) | | `getChildren` | `(item: T) => T[] \| undefined` | - | Return child rows. Enables expandable/tree rows | | `treeColumn` | `string` | - | Column id that displays the expand/collapse chevron | | `expandedKeys` | `DataGridSelection` | - | Controlled set of expanded row keys | | `defaultExpandedKeys` | `DataGridSelection` | - | Default expanded row keys (uncontrolled) | | `onExpandedChange` | `(keys: DataGridSelection) => void` | - | Callback when expanded rows change | | `treeIndent` | `number` | `20` | Pixels of indentation per nested level. `0` disables | ### DataGridColumn Column definition object passed to the `columns` prop. | Property | Type | Default | Description | |----------|------|---------|-------------| | `id` | `string` | - | Unique column identifier. Used as the sort key. Required | | `header` | `ReactNode \| (info) => ReactNode` | - | Header content. Render function receives `{ sortDirection }` | | `accessorKey` | `keyof T & string` | - | Key on `T` to read the cell value from | | `cell` | `(item: T, column) => ReactNode` | - | Custom cell renderer | | `isRowHeader` | `boolean` | `false` | Mark this column as the row header (for accessibility) | | `allowsSorting` | `boolean` | `false` | Allow this column to be sorted | | `sortFn` | `(a: T, b: T) => number` | - | Custom sort comparator | | `allowsResizing` | `boolean` | - | Allow resizing (with `allowsColumnResize` on the grid) | | `width` | `DataGridColumnSize` | - | Initial column width (px, %, or fr) | | `minWidth` | `number` | - | Minimum column width | | `maxWidth` | `number` | - | Maximum column width | | `align` | `'start' \| 'center' \| 'end'` | `'start'` | Cell text alignment | | `headerClassName` | `string` | - | Additional className for every `
` of this column | | `cellClassName` | `string` | - | Additional className for every `` of this column | | `pinned` | `'start' \| 'end'` | - | Pin this column. Requires numeric `width` or `minWidth` | ### DataGridReorderEvent Event object passed to the `onReorder` callback. | Property | Type | Description | |----------|------|-------------| | `keys` | `Set` | The keys that were moved | | `target` | `{ key: Key; dropPosition: 'before' \| 'after' }` | The target row key and drop position | | `reorderedData` | `T[]` | The full reordered data array after applying the move | ### Type Exports | Type | Description | |------|-------------| | `DataGridSelection` | A set of selected keys, or `'all'` | | `DataGridSortDescriptor` | The current sort column and direction | | `DataGridSortDirection` | `'ascending' \| 'descending'` | | `DataGridColumnSize` | A number, numeric string, percentage, or fractional unit | | `DataGridDragAndDropHooks` | Hooks returned by React Aria's `useDragAndDrop` | ## Related Components - **Table**: Structured data display in rows and columns - **Checkbox**: Binary choice input control - **Chip**: Compact elements for tags and filters