Columns Reference
Columns configuration, types, order, and visibility
Typed product columns
The following complete example builds on the quick start and shared recipe files. The reference below explains individual options and integration fragments.
import { defineTableConfig } from "@/components/ui/yayaw-table";
export const productConfig = defineTableConfig({
id: "products",
translations: { namespace: "products", keys: {} },
columns: {
definitions: [
{ id: "name", header: "Name", type: "text" },
{ id: "price", header: "Price", type: "number" },
{ id: "stock", header: "Stock", type: "number" },
{
id: "status",
header: "Status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
},
],
order: ["name", "price", "stock", "status"],
visible: ["name", "price", "stock", "status"],
mandatory: ["name"],
sort: [{ id: "name", desc: false }],
},
table: {
allowCreate: false,
allowEdit: false,
allowDelete: false,
allowDuplicate: false,
allowBulkEdit: false,
allowBulkDelete: false,
enableRowSelection: false,
enableViews: false,
syncUrl: false,
defaultPageSize: 10,
},
});
export const getTableConfig = (tableType: string) =>
tableType === "products" ? productConfig : undefined;import { defineTableConfig } from "@/components/ui/yayaw-table-vue";
export const productConfig = defineTableConfig({
id: "products",
translations: { namespace: "products", keys: {} },
columns: {
definitions: [
{ id: "name", header: "Name", type: "text" },
{ id: "price", header: "Price", type: "number" },
{ id: "stock", header: "Stock", type: "number" },
{
id: "status",
header: "Status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
},
],
order: ["name", "price", "stock", "status"],
visible: ["name", "price", "stock", "status"],
mandatory: ["name"],
sort: [{ id: "name", desc: false }],
},
table: {
allowCreate: false,
allowEdit: false,
allowDelete: false,
allowDuplicate: false,
allowBulkEdit: false,
allowBulkDelete: false,
enableRowSelection: false,
enableViews: false,
syncUrl: false,
defaultPageSize: 10,
},
});
export const getTableConfig = (tableType: string) =>
tableType === "products" ? productConfig : undefined;Columns Reference
Declare columns in columns.definitions, then control order and visibility via columns.order and columns.visible.
columns.order is the order the table starts in: the columns it lists, in that order, then the others in definition order, with select first and actions last. A saved view's order, or the person's once they move a column, replaces it. React follows it since v3.9.2; before, it started in definition order.
columns: {
definitions: [
{ id: 'name', type: 'text', header: 'Name', enableSorting: true, enableColumnFilter: true },
{
id: 'price',
type: 'number',
header: 'Price',
numberFormat: { thousandsSeparator: ' ', decimals: 0, suffix: ' €' },
},
{ id: 'imageUrl', type: 'image', header: 'Image', enableSorting: false, enableColumnFilter: false },
{ id: 'status', type: 'tag', header: 'Status' },
{ id: 'metadata', type: 'json', header: 'Metadata', maxItems: 4 },
{ id: 'externalUrl', type: 'url', header: 'External URL' },
{ id: 'createdAt', type: 'date', header: 'Created', dateDisplayPreset: 'dmy-numeric' },
{ id: 'isActive', type: 'boolean', header: 'Active' },
{ id: 'actions', type: 'actions', header: 'Actions' },
{ id: 'value', type: 'dynamicType', typeKey: 'valueType', header: 'Value' },
],
order: ['select', 'name', 'price', 'status', 'createdAt', 'actions'],
visible: ['select', 'name', 'price', 'status', 'createdAt', 'actions'],
mandatory: ['name'],
}Types
Supported column types:
textstringnumberbooleandateselectmultiSelecttagcodejsonlocation(a place{ lat, lng, label?, address? }, see Location columns)urlimage(URL string with an automatic fallback when empty or invalid)actionsdynamicType(renderer selected viatypeKey)
The row-selection checkbox is a virtual column with id select; it is separate
from data columns that use type: 'select'.
Options per column
Common properties:
id: string– Unique identifierheader: string | React.ReactNodeenableSorting?: boolean(defaulttrue)enableColumnFilter?: boolean(defaulttrue)enableResizing?: boolean– allow this column to resize whentable.enableColumnResizingis enabled (defaulttruefor data columns)size?: number,minSize?: number,maxSize?: number– initial and allowed widths in pixelsaccessorKey?: string– Optional accessor if data key differs fromiddateDisplayPreset?: DateDisplayPreset– strict preset for date rendering (date columns only)dateFormat?: string– legacy date-fns format fallback for date columnsenableCalculation?: boolean– enable/disable footer calculations for this column oncetable.enableCalculationsis enabled (defaulttrue, except system columns likeselect/actions)defaultCalculation?: CalculationType– default footer calculation shown for this column (validated against column type)inlineEdit?: boolean | InlineEditColumnConfig– enable/configure inline editing for this columnFor number columns:
numberFormat?: NumberFormatConfig– display format (thousands/decimal separators, decimals, prefix/suffix for currency). See Number column below.For json columns:
maxItems?: number– maximum object/array items shown before truncation.For string columns:
showQuotes?: boolean– render values with quote marks for token-like data.For tag columns:
tagColorMap?: Record<string, string>– optional value → Tailwind class map (see below).For image columns: values are URL strings. Relative app paths such as
/images/table-demo/products/laptop.svg,http(s)URLs,blob:URLs, and base64data:image/*URLs are accepted. Empty or invalid values render a compact fallback.
Column resizing
Enable resizing once at table level, then opt individual columns out when their layout must stay fixed:
table: {
enableColumnResizing: true,
},
columns: {
definitions: [
{ id: 'name', type: 'text', header: 'Name', size: 240, minSize: 140, maxSize: 420 },
{ id: 'status', type: 'tag', header: 'Status', enableResizing: false },
],
}The header separator supports pointer, touch, and keyboard input. Left/Right Arrow changes the width by 10 pixels, Home uses minSize, End uses maxSize, and double-click restores size. React and Vue persist the resulting widths in URL state and saved views. The virtual select and actions columns cannot be resized.
Footer calculations by column
You can preconfigure calculations directly on columns:
{
id: 'price',
type: 'number',
header: 'Price',
defaultCalculation: 'average',
}Useful defaults by type:
Any type:
count_all,count_values,count_unique,count_empty,count_not_empty,percent_empty,percent_not_emptyNumber:
sum,average,median,min,max,rangeDate:
min,max,rangeBoolean:
count_true,count_false,percent_true,percent_false
If a defaultCalculation is invalid for the column type, it is ignored safely.
Inline Edit by Column
Enable inline edit directly on a column:
{
id: 'name',
type: 'text',
header: 'Name',
inlineEdit: true,
}Advanced per-column config:
{
id: 'status',
type: 'tag',
header: 'Status',
inlineEdit: {
enabled: true,
editor: 'select',
formField: 'status',
debounceMs: 700,
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
},
}InlineEditColumnConfig fields:
enabled?: boolean– explicit on/off for this columneditor?: 'auto' | 'text' | 'number' | 'boolean' | 'date' | 'select' | 'multiSelect' | 'textarea' | 'json' | 'url' | 'location'debounceMs?: number– overrides table-level debounceformField?: string– form field name used for schema validation and update payload keyoptions?: Array<{ label: string; value: string | number | boolean; disabled?: boolean }>– select choicesreadonly?: boolean– force non-editable in inline mode
Inline multiple selection
In React and Vue, editor: 'multiSelect' opens a floating list anchored to a
single-line field. Selected values appear as removable chips, and typing in the
same field filters options by label. The popup does not expand the table row.
Arrow keys navigate choices; Enter toggles the highlighted option. Enter with no
highlighted option, Tab out of the editor, or a click outside saves the latest
selection. Escape discards changes that have not been sent yet.
Autosave uses debounceMs (700 ms by default). When
table.inlineEdit.showDelayIndicator is enabled (the default), a thin bar shows
the delay and remains visible while the save is pending. Dismissal waits for the
pending save and any newer draft. Failed writes keep the editor, draft and error
available for retry; a stale response never replaces a newer selection.
Disabled options cannot be added or removed. Numeric and boolean option values
keep their types in update payloads, and removing every option saves [].
Dismissing an unchanged editor sends no update.
Number column (number)
Number columns render numeric values with optional formatting and are right-aligned (header and cells) by default.
Display format: numberFormat
Use numberFormat to control thousands separator, decimal separator, number of decimals, and optional monetary prefix/suffix.
Presets (string):
'space'– space as thousands separator, dot for decimals (e.g.1 234 567.89)'dot'– dot for thousands, comma for decimals (e.g.1.234.567,89)'comma'– comma for thousands, dot for decimals (e.g.1,234,567.89)'locale'– use browserIntl.NumberFormat(locale-dependent)
Explicit options (object):
thousandsSeparator?: string– e.g.' ','.',','decimalSeparator?: string– e.g.'.',','decimals?: number– fixed decimal places (omit for default)prefix?: string– text before the number (e.g.'€ 'for currency)suffix?: string– text after the number (e.g.' €'for euros)style?: 'decimal' | 'currency' | 'percent' | 'compact' | 'unit'– Intl style; settingcurrencyimplies'currency'currency?: string,currencyDisplay?: 'symbol' | 'narrowSymbol' | 'code' | 'name'– ISO 4217 code such as'EUR'unit?: string,unitDisplay?: 'short' | 'long' | 'narrow'– Intl unit such as'kilogram'locale?: string– overrides the table locale for this columnminimumFractionDigits?,maximumFractionDigits?– bounded decimals (decimalsfixes both; Vue'sdecimalPlacesis accepted)signDisplay?: 'auto' | 'always' | 'exceptZero' | 'never'negative?: 'minus' | 'parentheses'–'parentheses'shows(1,234.00), as in accountingpercentBase?: 'fraction' | 'whole'– percent input0.25(default) or25for 25%display?: 'number' | 'bar',max?: number– a progress bar beside the value, full atmax(1 for fraction percents, 100 otherwise)
Custom separators apply to every style, including currencies and percents.
Examples:
// Euros, no decimals, space thousands (e.g. 2 499 €)
{
id: 'price',
type: 'number',
header: 'Price',
numberFormat: { thousandsSeparator: ' ', decimals: 0, suffix: ' €' },
}
// US-style with prefix
{
id: 'amount',
type: 'number',
header: 'Amount',
numberFormat: { thousandsSeparator: ',', decimalSeparator: '.', decimals: 2, prefix: '$ ' },
}
// Preset only
{ id: 'count', type: 'number', header: 'Count', numberFormat: 'space' }
// Currency by locale: €1,234.50, or 1 234,50 € with locale 'fr-FR'
{ id: 'revenue', type: 'number', header: 'Revenue', numberFormat: { currency: 'EUR' } }
// Accounting negatives: ($1,234.50)
{ id: 'balance', type: 'number', header: 'Balance', numberFormat: { currency: 'USD', negative: 'parentheses' } }
// Progress bar from a 0–1 value: ▰▰▱ 49%
{ id: 'progress', type: 'number', header: 'Progress', numberFormat: { style: 'percent', display: 'bar' } }
// Compact and units: 1.3M, 12 kg
{ id: 'views', type: 'number', header: 'Views', numberFormat: { style: 'compact' } }
{ id: 'weight', type: 'number', header: 'Weight', numberFormat: { style: 'unit', unit: 'kilogram' } }When numberFormat is not set, React shows plain values (no thousands grouping) and Vue groups them by locale; set numberFormat for identical output in both editions. A custom formatter (if provided when building columns programmatically) overrides numberFormat.
Date display presets
For type: 'date', you can use strict presets:
localized-shortlocalized-mediumlocalized-longmonth-name-longmonth-yeardmy-numericdmy-shortmdy-numericmdy-shortiso-dateiso– full ISO instantdate,short,long– medium, short and full localized datesdateTime– date and timetime– time onlyrelative– localized distance such as “3 days ago” or “il y a 3 jours”
Date columns also accept timeZone (IANA zone such as 'Europe/Paris', used by presets) and hour12 to force a 12- or 24-hour clock. A dateFormat pattern (date-fns) wins over the preset and uses local time. Date-only values such as '2026-09-10' are local calendar days.
Example:
{
id: 'createdAt',
type: 'date',
header: 'Created',
dateDisplayPreset: 'month-name-long',
}Tag column (tag)
Tag columns render values as colored badges. Colors are deterministic: the same tag value always gets the same color across sessions and page reloads (derived from a hash of the value). Values are normalized (trim + lowercase) for matching, so e.g. "Urgent" and "urgent" share the same color.
Custom colors per value: tagColorMap
You can override colors for specific tag values by passing a tagColorMap in the column definition. Keys are tag values (lookup tries the raw value, then the normalized value); values are Tailwind classes for the badge (e.g. background + text).
This works directly from columns.definitions without custom column wiring.
Example:
{
id: 'status',
type: 'tag',
header: 'Status',
tagColorMap: {
Urgent: 'bg-red-500/80 text-white dark:bg-red-600/90',
Done: 'bg-green-500/80 text-white dark:bg-green-600/90',
'In progress': 'bg-amber-500/80 text-white dark:bg-amber-600/90',
},
}Entries in tagColorMap use the given class; any other value falls back to the deterministic hash-based color.
Tags columns
A column with tags: true (or tags: { create?, manage?, bulk? }) holds tag
ids from your application's tag catalog: a multiSelect column holds a list
of them, a select column one. With the optional tags actions, the
column's options are the catalog, loaded once per table and column when the
table mounts and cached, so cells, cards, the Feed, filters (with color
swatches), grouping, forms and the record view show the tags' names and
colors. Without them, a tags column keeps its static options.
columns: [
{ id: "tags", header: "Tags", type: "multiSelect", tags: true, inlineEdit: true },
],
// getTableActions(tableType)
tags: {
list: ({ tableId, tableType, columnId }) => listTags(columnId), // [{ id, name, color? }]
create: ({ columnId, name, color }) => createTag(columnId, name, color), // the new tag
update: ({ id, name, color }) => updateTag(id, { name, color }), // color: null clears it
merge: ({ sourceIds, targetId }) => mergeTags(sourceIds, targetId), // rewrites the records
remove: ({ id }) => deleteTag(id), // and removes it from the records
},| Action | Receives, besides { tableId, tableType, columnId } | Without it |
|---|---|---|
list | nothing | The column's static options |
create | name, color?; answers the new tag | No "Create “name”" |
update | id, name?, color? (null clears it) | No rename or recolor |
merge | sourceIds, targetId | No merge |
remove | id | No delete |
Answers are values or { success, data?, error? }. The same actions and
column options work in React and Vue.
Tag picker (inline cells, record form fields on a tags column, the bulk dialogs): colored chips, a search that ignores case and accents, and "Create “name”" with
create(unlesstags: { create: false }), which creates, selects and caches the tag at once. Arrows, Enter, Backspace and Escape work in it.Bulk Add tags and Remove tags in the bulk bar (list columns, with
allowBulkEditandbulkUpdateorupdate): the selected rows change at once; rows that fail are restored and stay selected. By defaultbulkUpdate(ids, { [field]: tagIds })receives each group of rows' resulting lists; withtags: { bulk: "patch" }it receives{ [field]: { add, remove } }once, for your server to apply withapplyTagPatch().Manage tags in the column menu renames, recolors, merges and deletes tags; a deletion states how many records use the tag (one
aggregatecall grouped by the column).table.canManageTags: falseortags: { manage: false }hide it.Colors are palette names (
TAG_COLOR_NAMES) or any CSS color; a tag without a color keeps its automatic hue, andcoloredTags: falseshows neutral tags.Facets: a tags column in
table.facetslists the catalog's names, counts each tag once per record and filters withcontains.
The contract types and helpers (resolveTagColumn, tagOptions,
applyTagPatch, tagUsageRequest…) are in the server-safe
utils/tag-catalog.ts of both editions. Tag labels have built-in English and
French, chosen from the table's locale, and read tags.<key> translations.
Dynamic type columns
Use type: 'dynamicType' and specify typeKey on the row to pick the renderer at runtime.
Selection column (select)
The
selectcolumn is virtual and does not need a definition entry.Add
'select'tocolumns.orderand tocolumns.visibleto show it when you provide an explicit visible list.Controlled by
table.enableRowSelection(defaulttrue).Rendered as a narrow fixed-width column with a centered checkbox; same width as the actions column.
To hide it, remove
'select'fromcolumns.visible.It is not toggleable from the options menu UI.
Actions column (actions)
Add
{ id: 'actions', type: 'actions', header: '...' }tocolumns.definitions(the header label is not shown in the UI; useheader: ''or any label for accessibility/column menu).Rendered as a narrow fixed-width column with a centered actions menu (ellipsis) per row; same width as the selection column.
To hide it, remove
'actions'fromcolumns.visible.It is not toggleable from the options menu UI.
See also:
Declare the data type once
A column's type and options are the defaults for its cell, inline editor, generated create/edit/bulk form, and filter controls in both React and Vue. No matching FormConfig is required for standard fields. Enable the relevant table actions and supply their handlers; the library generates the controls. Registered form catalogues remain explicit overrides for validation, permissions, relations, and custom layouts.
| Column type | Cell | Inline / generated form | Filter |
|---|---|---|---|
text, string | Text | Text input | Text operators |
code | Monospace text | Multiline editor | Text operators |
number | Formatted number | Numeric input | Numeric comparisons and range |
boolean | Boolean indicator | Boolean control | True/false choices |
date | Date preset | Calendar date input | Calendar comparisons and range |
url | Link | URL input | Text operators |
image | Image with fallback | Image URL input | Text operators |
json | Structured value | JSON editor | Text operators |
location | Place label, else address, else coordinates | Address search and coordinates | Empty, within N km, within an area |
select, tag | Option label | Single selection | Membership operators |
multiSelect | Option labels | Multiple selection | Any/all/none membership |
dynamicType | Resolved from the row's typeKey | Resolved from that same row type | Text operators across mixed types |
custom | Application renderer | Explicit catalogue field or inline editor | Text operators or application filter |
actions | Row actions | No data editor | No data filter |
const columns = [
{ id: "name", header: "Name", type: "text" },
{ id: "themeIds", header: "Themes", type: "multiSelect", options: [
{ label: "Culture", value: 1 },
{ label: "Technology", value: 2 },
] },
{ id: "metadata", header: "Metadata", type: "json" },
{ id: "publishedOn", header: "Published on", type: "date" },
];Option labels are displayed while saved values retain their primitive identities: 1 and "1" are different choices. Unknown stored choices and whitespace are preserved. Empty numeric/date inline values save as null; dates edited inline save as YYYY-MM-DD in both editions, matching generated date forms. React integrations that previously expected a Date from inline editing should accept the date-only string in their field schema and update handler.
JSON drafts remain visible while incomplete and cannot be submitted until valid. Parsed objects, arrays, strings, numbers, booleans and null reach the update action. Enter inserts a newline in code/JSON editors; Ctrl/Cmd+Enter or leaving the editor commits, and Escape cancels the draft. Existing schemas and failed-save feedback still apply.
Computed accessorFn columns and custom values are not assigned a generated text field. Describe their write contract explicitly in a catalogue. Mixed dynamic types are excluded from generated bulk fields when the selected rows do not share a type. An explicit catalogue remains authoritative: omitted, hidden or disabled fields do not become editable merely because a column exists.
Show only inventory fields
Use a smaller initial column set without removing the other properties from the catalog. Users can restore optional columns from Properties.
Copy this complete configuration beside product-config.ts. Use () => exampleConfig as React’s getTableConfig, or :config="exampleConfig" in Vue.
Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table/config/helpers";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
columns: {
...productConfig.columns,
visible: ["name", "stock", "status"],
order: ["name", "stock", "status", "price"],
},
});Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table-vue/config";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
columns: {
...productConfig.columns,
visible: ["name", "stock", "status"],
order: ["name", "stock", "status", "price"],
},
});Add booleans and tags
Declare the underlying types instead of formatting everything as strings. The sample records already contain active and tags values.
Copy this complete configuration beside product-config.ts. Use () => exampleConfig as React’s getTableConfig, or :config="exampleConfig" in Vue.
Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table/config/helpers";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
columns: {
...productConfig.columns,
definitions: [
...productConfig.columns.definitions,
{ id: "active", header: "Active", type: "boolean" },
{
id: "tags",
header: "Tags",
type: "multiSelect",
options: [
{ value: "office", label: "Office" },
{ value: "wood", label: "Wood" },
{ value: "seating", label: "Seating" },
],
},
],
visible: ["name", "active", "tags"],
order: ["name", "active", "tags", "price", "stock", "status"],
},
});Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table-vue/config";
import { productConfig } from "./product-config";
export const exampleConfig = defineTableConfig({
...productConfig,
columns: {
...productConfig.columns,
definitions: [
...productConfig.columns.definitions,
{ id: "active", header: "Active", type: "boolean" },
{
id: "tags",
header: "Tags",
type: "multiSelect",
options: [
{ value: "office", label: "Office" },
{ value: "wood", label: "Wood" },
{ value: "seating", label: "Seating" },
],
},
],
visible: ["name", "active", "tags"],
order: ["name", "active", "tags", "price", "stock", "status"],
},
});Application filter controls
filterRenderer({ value, onChange }) optionally supplies a React node or Vue
VNode for a column in the native filter menu. Use it for domain date ranges or
remote relation selectors. The callback writes the ordinary column-filter state,
so saved views and reset share the same value. Return undefined or an empty
string to clear a filter. Preserve typed IDs and boolean values. The application
owns accessible labels, input validation, loading, cancellation and API mapping.
This does not automatically add the control to the optional quick-filter bar.