gridcn1.2.2

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.

Name
Email
Age
Active
Role
Joined
Score
Silent Tiger
user0@example.com
63
Viewer
Jul 5, 2023
4
Quick Lion
user1@example.com
67
Viewer
Apr 3, 2021
89
Swift Wolf
user2@example.com
40
Manager
Oct 27, 2020
80
Eager Lion
user3@example.com
50
User
Oct 8, 2022
51
Eager Tiger
user4@example.com
45
Viewer
Oct 7, 2024
83
Silent Wolf
user5@example.com
24
User
Jun 1, 2020
15
Swift Lion
user6@example.com
34
Viewer
Oct 18, 2022
62
Eager Lion
user7@example.com
21
Manager
Oct 7, 2021
42
Eager Eagle
user8@example.com
25
Editor
Jun 11, 2024
35
Bold Tiger
user9@example.com
19
Editor
Mar 20, 2024
52
Bold Bear
user10@example.com
42
User
Apr 16, 2020
28
Swift Wolf
user11@example.com
47
Manager
Aug 4, 2022
9

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.

Count
Code
Item
Label
1
001
Item 1
West
2
002
Item 2
West
3
003
Item 3
West
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.

Count
Code
Item
Label
1
001
Item 1
West
2
002
Item 2
West
3
003
Item 3
West

If the source cells form a detectable series, the fill extrapolates it instead of tiling:

  • Arithmetic runs. 1, 2, 3 becomes 4, 5, 6. 10, 20, 30 becomes 40, 50, 60. This also works with a single-column decimal step.
  • Zero-padded numbers. 001, 002 becomes 003, 004, with width preserved.
  • Text plus trailing number. Item 1 becomes Item 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

New

After 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.

On this page