Skip to content

Columns

The columns prop defines every column in the grid. Each entry is a DataEditorColumn object.

<DataEditor columns={columns} ... />
type DataEditorColumn = {
id: string;
title: string;
editor?: CellEditor;
validators?: ValidatorRule[];
unique?: boolean;
dependentFields?: string[];
formatter?: (value: string) => string;
transformer?: (value: unknown) => unknown;
filter?: ColumnFilter;
pinnable?: boolean;
size?: number;
locked?: boolean | "all" | "default";
};
Typestring
RequiredYes

Unique column identifier. Must match the keys in your row data.

Typestring
RequiredYes

Column header text shown to the user.

TypeCellEditor
Default{ type: "text" }

Controls how the cell is edited. Four types available:

type CellEditor =
| { type: "text" }
| { type: "date"; minDate?: Date; maxDate?: Date }
| { type: "select"; options: string[] }
| { type: "number"; decimalPlaces?: number; decimalSeparator?: string; thousandsSeparator?: string; allowChars?: string };

Plain text input.

{ type: "text" }

Date picker with optional min/max bounds.

{ type: "date", minDate: new Date("2020-01-01"), maxDate: new Date("2030-12-31") }

Dropdown with a fixed list of options. Each string is both the stored value and the display label.

{ type: "select", options: ["Admin", "Editor", "Viewer"] }

Number input with locale-aware formatting.

{
type: "number",
decimalPlaces: 2,
decimalSeparator: ".",
thousandsSeparator: ",",
allowChars: "%-",
}
FieldTypeDescription
decimalPlacesnumberMaximum decimal digits. Unrestricted when omitted.
decimalSeparatorstringDecimal point character. Defaults to browser locale.
thousandsSeparatorstringThousands grouping character. Defaults to browser locale.
allowCharsstringExtra characters to allow beyond digits, decimal separator, and minus sign.
TypeValidatorRule[]

One or more validators run on every edit. Each entry is a tagged object describing a rule. Built-in rules cover the common cases; the function rule is the escape hatch for anything custom.

type ValidatorRule =
| BuiltInValidator
| { type: "function"; fn: CellValidator };
type ValidationError = {
level: "error";
message: string;
};

A ValidationError with level: "error" flags the cell in the grid but does not block submission — invalid rows are delivered to onComplete alongside valid ones, tagged via the isValid flag.

Each rule is an object literal. The optional message overrides the default localized error text.

Rejects empty, null, or undefined values.

{ type: "required", message: "Name is required" }

Rejects non-numeric values. Handles comma and dot separators.

{ type: "numeric", message: "Must be a number" }

Validates email format.

{ type: "email", message: "Invalid email address" }

Validates YYYY-MM-DD or DD/MM/YYYY formats. Pass format to require a specific one.

{ type: "date", format: "YYYY-MM-DD", message: "Invalid date" }

Restricts to a set of allowed values.

{ type: "oneOf", values: ["Active", "Inactive"], message: "Must be Active or Inactive" }

Validates against a regular expression. pattern is a string compiled at runtime; flags is optional.

{ type: "regex", pattern: "^\\+[\\d\\s]+$", message: "Invalid phone number" }

Restricts a numeric value to a min/max range. Both bounds are optional.

{ type: "range", min: 0, max: 1_000_000, message: "Must be between 0 and 1,000,000" }

When a built-in doesn’t fit, drop down to { type: "function" }. The fn receives the cell value and the full row; return a ValidationError to flag a problem, or null if valid.

type CellValidator = (value: unknown, row: DataEditorRow) => ValidationError | null;
{
type: "function",
fn: (value, row) => row.country === "US" && !/^\d{5}$/.test(String(value))
? { level: "error", message: "US ZIP must be 5 digits" }
: null,
}

Use dependentFields together with a function rule to revalidate this column when another column changes:

{
id: "endDate",
title: "End Date",
editor: { type: "date" },
validators: [{
type: "function",
fn: (value, row) =>
new Date(String(value)) <= new Date(String(row.startDate))
? { level: "error", message: "End date must be after start date" }
: null,
}],
dependentFields: ["startDate"],
}
Typeboolean

When true, the editor flags duplicate values in this column as errors. The error message is localized via the translations prop (dataEditor.validation.valueMustBeUnique).

Typestring[]

Column IDs to revalidate when this column changes.

Type(value: string) => string

Format the display value without changing stored data.

{ formatter: (v) => v ? `$${v}` : "" }
Type(value: unknown) => unknown

Transform a value before it enters the store. Runs when rows are uploaded to the data editor.

{ transformer: (v) => typeof v === "string" ? v.trim() : v }
TypeColumnFilter

Adds a filter control for this column in the sidebar Filters panel.

type ColumnFilter =
| { type: "select"; label?: string; placeholder?: string; options?: string[]; multiple?: boolean }
| { type: "number-range"; label?: string }
| { type: "date-range"; label?: string };

Dropdown to pick one or more values.

{ type: "select", label: "Status", placeholder: "All statuses", options: ["Active", "Inactive"], multiple: true }
FieldTypeDescription
labelstringLabel above the filter.
placeholderstringPlaceholder when nothing selected.
optionsstring[]Fixed options. When omitted, derived from column values.
multiplebooleanAllow multiple selections.

Two inputs for min and max.

{ type: "number-range", label: "Salary Range" }

Two date pickers for start and end date.

{ type: "date-range", label: "Date Range" }
Typeboolean
Defaulttrue

Whether this column can be pinned to the left (right in RTL) via the header context menu.

Typenumber
Default150

Column width in pixels.

Typeboolean | "all" | "default"

Controls whether cells in this column are locked.

  • true or "all" — locked for every row.
  • "default" — locked only for default-source rows. Rows added manually, duplicated, or imported remain editable.
  • false or undefined — not locked.

A column locked via configuration cannot be unlocked from the UI.

import type { DataEditorColumn } from "@updog/data-editor";
const columns: DataEditorColumn[] = [
{
id: "name",
title: "Full Name",
size: 200,
validators: [{ type: "required", message: "Name is required" }],
transformer: (v) => typeof v === "string" ? v.trim() : v,
},
{
id: "email",
title: "Email",
size: 250,
validators: [
{ type: "required", message: "Email is required" },
{ type: "email", message: "Invalid email" },
],
unique: true,
},
{
id: "salary",
title: "Salary",
editor: { type: "number", decimalPlaces: 2 },
validators: [{ type: "numeric", message: "Must be a number" }],
formatter: (v) => v ? `$${v}` : "",
filter: { type: "number-range", label: "Salary" },
},
{
id: "role",
title: "Role",
editor: { type: "select", options: ["Admin", "Editor", "Viewer"] },
validators: [{ type: "oneOf", values: ["Admin", "Editor", "Viewer"], message: "Invalid role" }],
filter: { type: "select", label: "Role", multiple: true },
},
{
id: "startDate",
title: "Start Date",
editor: { type: "date" },
filter: { type: "date-range", label: "Start Date" },
},
{
id: "endDate",
title: "End Date",
editor: { type: "date" },
validators: [{
type: "function",
fn: (value, row) =>
new Date(String(value)) <= new Date(String(row.startDate))
? { level: "error", message: "Must be after start date" }
: null,
}],
dependentFields: ["startDate"],
},
{
id: "notes",
title: "Notes",
size: 300,
locked: false,
pinnable: false,
},
];