Fill handle
Drag to tile a selection, with series inference for numeric and text runs.
The fill handle is the small square at the bottom-right corner of a selection. Drag it up, down, left, or right to extend the selection and fill the new cells from the source range. The drag snaps to whichever axis you move further on. Once a fill commits, the cells it wrote play a brief fade pulse so the changed values stand out. The pulse only plays for cells in the viewport and never replays when a cell scrolls back into view.
The hero grid above shows the handle in action. The patterns demo below shows the series inference on its own:
Select a cell or range in a filled column and drag the small handle at its corner down: Count (1, 2, 3), Code (001, 002) and Item (Item 1, Item 2) extrapolate their series, Label (plain text) just tiles. Hold Alt/Option while dragging to force a plain copy, or select a filled range and press Ctrl/Cmd+D (fill down) / Ctrl/Cmd+R (fill right) instead of dragging.
pnpm dlx shadcn@latest add @gridcn/data-grid-fill
Mouse-only, hidden from assistive tech
The fill handle itself has no keyboard equivalent and is hidden from screen readers. Keyboard users get the same results with Ctrl/Cmd+D or Ctrl/Cmd+R, covered below.
Wiring it up
The fill handle lives in the separate data-grid-fill add-on. useDataGridFill() builds one
plugin: its Overlay paints the drag preview and the handle, and its Mount runs the pointer
engine (see Plugins for the seam). The keyboard fill shortcuts arrive as the
plugin's fillDown/fillRight commands.
"use client";
import { DataGridProvider, DataGridRoot, DataGridHeader, DataGridBody } from "@/components/data-grid/data-grid";
import { useDataGridFill } from "@/components/data-grid-fill/data-grid-fill";
function MyGrid() {
const { plugin } = useDataGridFill();
return (
<DataGridProvider data={data} columns={columns} getRowId={getRowId} plugins={[plugin]}>
<DataGridRoot>
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</DataGridProvider>
);
}Composing with core's DataGrid convenience wrapper
<DataGrid> does not wire fill in automatically. Pass the plugin through plugins (see the
hero demo above for the full pattern). This is the same way you compose any other add-on.
Tiling vs. series inference
By default, dragging tiles the source selection modulo its size into the target. A single cell repeats, a 2-row pattern repeats every 2 rows, and so on.
Select a cell or range in a filled column and drag the small handle at its corner down: Count (1, 2, 3), Code (001, 002) and Item (Item 1, Item 2) extrapolate their series, Label (plain text) just tiles. Hold Alt/Option while dragging to force a plain copy, or select a filled range and press Ctrl/Cmd+D (fill down) / Ctrl/Cmd+R (fill right) instead of dragging.
If the source cells form a detectable series, the fill extrapolates it instead of tiling:
- Arithmetic runs.
1, 2, 3becomes4, 5, 6.10, 20, 30becomes40, 50, 60. This also works with a single-column decimal step. - Zero-padded numbers.
001, 002becomes003, 004, with width preserved. - Text plus trailing number.
Item 1becomesItem 2,Item 3, and so on.
A number with more than 15 digits is an identifier, not a count. The fill tiles such a value and does not extrapolate it.
Series detection needs two source cells
A single source cell always tiles, that is, copies, rather than extrapolating. There is no delta to detect from one value.
Forcing a plain copy
Hold Alt (Option on Mac) while you drag the fill handle. This forces plain tiling, even over a detected series. Excel uses the same convention.
Custom series detection
The built-in detector covers arithmetic runs, zero-padded numbers, and text plus a trailing
number. Date series (2026-01-01, 2026-01-02 → 2026-01-03) are not detected. Pass detectSeries
to useDataGridFill to replace the built-in detector; null returns fall back to tiling, the
same as an undetected series. The callback receives the source cells along the fill axis (per
source column for vertical fills, per source row for horizontal fills) in source order, and
returns a SeriesDescriptor whose extrapolate(index) computes the value at any source-relative
index:
import { useDataGridFill, type SeriesDescriptor } from "@/components/data-grid-fill/data-grid-fill";
const detectDates = (values: readonly string[]): SeriesDescriptor | null => {
if (values.length < 2) return null;
const first = Date.parse(values[0]!);
const step = Date.parse(values[1]!) - first;
if (values.some((v) => Number.isNaN(Date.parse(v)))) return null;
for (let i = 2; i < values.length; i += 1) {
if (Date.parse(values[i]!) - Date.parse(values[i - 1]!) !== step) return null;
}
return { extrapolate: (index) => new Date(first + step * index).toISOString().slice(0, 10) };
};
function MyGrid() {
const { plugin } = useDataGridFill({ detectSeries: detectDates });
// ...
}To extend the built-in detector instead of replacing it, check your own series first and return
the built-in detectSeries result for everything else:
import { detectSeries, useDataGridFill } from "@/components/data-grid-fill/data-grid-fill";
const detectDatesOrBuiltIn = (values: readonly string[]) => detectDates(values) ?? detectSeries(values);
function MyGrid() {
const { plugin } = useDataGridFill({ detectSeries: detectDatesOrBuiltIn });
// ...
}Keyboard fill
- Ctrl/Cmd+D: fill down. This copies the top row of the current selection into every row below it.
- Ctrl/Cmd+R: fill right. This copies the left column into every column to its right.
Both go through the same tiling and series pipeline as a drag. The keys are core bindings: their
preventDefault runs with or without the add-on (mod+R never reloads the page). Without the
add-on installed, both keys do nothing. The add-on's fillDown/fillRight commands give them
their body. See Pluggable built-ins.
Touch
NewAfter a touch selection the fill handle is hidden, because the selection handle sits on the same
corner. The cell context menu shows Fill down and Fill right instead. They run the same
fillDown and fillRight commands as the keyboard.
Turning fill off
useDataGridFill({ disabled: true }) turns the whole feature off (handle hidden,
fillDown/fillRight no-op). A read-only grid (readOnly on DataGrid) also disables fill,
with or without the option. Within a partially writable grid, a fill over readOnly columns
(per-column readOnly, function or boolean) fills the writable columns and silently skips the
readOnly ones, the same skip-no-error discipline as a paste over a non-writable cell.
On a lazy grid, a fill stops at loaded rows
A fill target that covers unloaded rows fills the loaded rows and skips the unloaded cells. The selection still expands over the whole target. A dev warning names the skipped rows. See Lazy loading.
onFill
Intercept or veto a fill before it applies. This fires on drag release and on the keyboard fill
actions, and passes straight through the options of useDataGridFill:
import { useDataGridFill, type FillArgs } from "@/components/data-grid-fill/data-grid-fill";
function onFill({ source, target, values, preventDefault }: FillArgs) {
// values is the pattern being written into the target strip (target.height x target.width),
// extrapolated or tiled, before validation runs
if (someCondition) preventDefault(); // nothing is applied, selection doesn't expand
}
function MyGrid() {
const { plugin } = useDataGridFill({ onFill });
// ...
}A fill, like a paste, is exactly one onDataChange call with a single operations batch, regardless of
how many cells it touched.