Input Grid
A rows × cols grid of typed text inputs. Each cell is a real <input> so native form semantics, autocomplete, autofill, and IME composition work out of the box. Tab walks reading-order; arrow keys move between cells; Home/End jump to row ends; PageUp/PageDown jump to column ends.
Customize
Shape
3x3
off
0.50rem
Installation
npx shadcn@latest add https://craftbits.dev/r/input-grid.jsonUsage
import { useState } from "react";
import { InputGrid } from "@craft-bits/core";
const [values, setValues] = useState<string[][]>([
["", "", ""],
["", "", ""],
]);
<InputGrid
rows={2}
cols={3}
values={values}
onValuesChange={setValues}
placeholder="0"
aria-label="2x3 matrix"
/>Uncontrolled — pass defaultValues and skip values/onValuesChange:
<InputGrid
rows={3}
cols={3}
defaultValues={[
["1", "0", "0"],
["0", "1", "0"],
["0", "0", "1"],
]}
aria-label="Identity matrix"
/>Per-column input type + label, per-cell disabling:
<InputGrid
rows={4}
cols={2}
columnsConfig={{
0: { label: "Key", type: "text", placeholder: "name" },
1: { label: "Value", type: "number", placeholder: "0" },
}}
cells={{
"0:0": { disabled: true },
}}
aria-label="Settings"
/>Understanding the component
- Cell anatomy. Every cell is a native
<input>wrapped in arole="gridcell"div. The grid container isrole="grid"witharia-rowcount/aria-colcountreflecting the label tracks when shown. - Override layering. Per-cell wins over per-row wins over per-column wins over grid defaults.
disabledis the only field that ORs upward — disabling the grid disables every cell regardless of per-cell config. - Controlled vs uncontrolled. Pass
values+onValuesChangefor controlled. PassdefaultValuesfor uncontrolled — internal state projects onto the currentrows × colsshape on resize so existing input is preserved when the grid grows or shrinks. - Keyboard model. Tab walks reading-order (left-to-right, top-to-bottom). Arrow keys move one cell at a time and wrap within the row (left/right) or column (up/down). Home / End jump to the first/last enabled cell in the row; Ctrl+Home / Ctrl+End and PageUp / PageDown jump to the first/last enabled cell in the column. Disabled cells are skipped during navigation.
- Labels.
rowsConfig[r].labelrenders a left header column;columnsConfig[c].labelrenders a top header row. Either auto-shows when any row or column carries a label; force-show or force-hide viashowRowLabels/showColumnLabels.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | required | Number of rows. |
cols | number | required | Number of columns. |
values | readonly (readonly string[])[] | — | Controlled value matrix. Pair with onValuesChange. |
defaultValues | readonly (readonly string[])[] | — | Uncontrolled initial value matrix. |
onValuesChange | (values: string[][]) => void | — | Fired on any cell change. |
type | HTMLInputTypeAttribute | 'text' | Default input type for every cell. |
placeholder | string | — | Default placeholder for every cell. |
disabled | boolean | false | Disable every cell. |
rowsConfig | Record<number, InputGridRowConfig> | — | Row-level overrides (label, placeholder, disabled, type). |
columnsConfig | Record<number, InputGridColumnConfig> | — | Column-level overrides (label, placeholder, disabled, type). |
cells | Record<string, InputGridCell> | — | Sparse per-cell overrides keyed by 'row:col'. |
showRowLabels | boolean | auto | Force the row-label column on/off. |
showColumnLabels | boolean | auto | Force the column-header row on/off. |
gap | string | '0.5rem' | Gap between cells (CSS length). |
minCellWidth | string | '0' | Minimum width per cell (CSS length). |
aria-label | string | — | Accessible name for the grid. |
className | string | — | Merged onto the rendered root <div>. |
Accessibility
- Renders
<div role="grid">witharia-rowcount/aria-colcountcovering label tracks. Each row of cells is arole="gridcell"wrapper around a native<input>. Optional row/column labels userole="rowheader"/role="columnheader". - Always pass
aria-label(oraria-labelledby) on the grid so screen readers announce its purpose. - Each cell computes an accessible name as
aria-label || gridLabel — rowLabel — colLabel || 'Row N, column M'. Override per cell withcells['r:c']['aria-label']. - Keyboard: arrow keys wrap within row/column and skip disabled cells; Tab walks reading-order; Home/End scope to the row, PageUp/PageDown (and Ctrl+Home / Ctrl+End) scope to the column.
- Focus is visible via a 2-px accent ring keyed on
--cb-accent. Disabled cells drop from tab order and from arrow navigation. - Native
<input>autocomplete, autofill, IME, copy/paste, and form submission all work — the component does not steal these behaviors.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/interaction/InputGrid.tsx). The source path was empty in AlgoFlashcards — implemented from concept. Lesson chrome (typed-fact bindings, per-step answer scoring) was stripped; the primitive is a generalized typed input matrix with controlled+uncontrolled values, layered overrides, and full keyboard navigation.