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[]; enableCustomValue?: boolean }
| { type: "multiselect"; options: string[]; enableCustomValue?: boolean; delimiter?: 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"] }

By default (enableCustomValue true), values outside options are accepted in the editor: users can add new options inline (in the value-matching step and the grid), created options persist on the column, and off-list values appear in the column filter. On import a select value is kept only if it is mapped, so to keep an off-list value the user maps it to an existing option or explicitly creates a new option for it; values left unmatched are dropped. Off-list values are never added as options implicitly. The SDK does not validate membership; add a oneOf validator if you want to constrain the value. Set enableCustomValue: false for a strict closed enum: no inline option creation, and values can only map to an existing option.

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

Dropdown where users pick zero or more options. The stored cell value is a string[], not a string.

{ type: "multiselect", options: ["red", "green", "blue"] }

The grid paints the values as joined text (red, blue), and exports join the array with the column delimiter (default ", "). Filtering and search match per element: a cell ["red", "blue"] matches a red value filter and a blue text search.

enableCustomValue works as it does for select (defaults to true): users can create options inline, and off-list values are kept only when mapped. A oneOf validator is applied per element, so every value in the array must be an allowed option for the row to pass.

On import, a raw cell holding several values is split into tokens. The delimiter is auto-detected among ,, ;, |, newline, and tab by matching tokens against your options. Set delimiter explicitly when the file’s vocabulary does not resemble your options (for example file values red, green against option codes R/G/B), where auto-detection cannot infer the separator. A cell that is itself a whole option containing the delimiter (option "Smith, Jr") is never split.

{ type: "multiselect", options: ["red", "green", "blue"], delimiter: ";" }

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}` : "" }

On a multiselect column the formatter runs per element: it receives one option at a time, never the joined string. The SDK formats each value then joins the results, so an option-to-label formatter ((v) => labels[v]) works unchanged.

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,
},
];