Docs
Display and explore

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.

product-config.ts
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;
product-config.ts
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:

  • text

  • string

  • number

  • boolean

  • date

  • select

  • multiSelect

  • tag

  • code

  • json

  • location (a place { lat, lng, label?, address? }, see Location columns)

  • url

  • image (URL string with an automatic fallback when empty or invalid)

  • actions

  • dynamicType (renderer selected via typeKey)

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 identifier

  • header: string | React.ReactNode

  • enableSorting?: boolean (default true)

  • enableColumnFilter?: boolean (default true)

  • enableResizing?: boolean – allow this column to resize when table.enableColumnResizing is enabled (default true for data columns)

  • size?: number, minSize?: number, maxSize?: number – initial and allowed widths in pixels

  • accessorKey?: string – Optional accessor if data key differs from id

  • dateDisplayPreset?: DateDisplayPreset – strict preset for date rendering (date columns only)

  • dateFormat?: string – legacy date-fns format fallback for date columns

  • enableCalculation?: boolean – enable/disable footer calculations for this column once table.enableCalculations is enabled (default true, except system columns like select/actions)

  • defaultCalculation?: CalculationType – default footer calculation shown for this column (validated against column type)

  • inlineEdit?: boolean | InlineEditColumnConfig – enable/configure inline editing for this column

  • For 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 base64 data: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.

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_empty

  • Number: sum, average, median, min, max, range

  • Date: min, max, range

  • Boolean: 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 column

  • editor?: 'auto' | 'text' | 'number' | 'boolean' | 'date' | 'select' | 'multiSelect' | 'textarea' | 'json' | 'url' | 'location'

  • debounceMs?: number – overrides table-level debounce

  • formField?: string – form field name used for schema validation and update payload key

  • options?: Array<{ label: string; value: string | number | boolean; disabled?: boolean }> – select choices

  • readonly?: 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 browser Intl.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; setting currency implies '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 column

  • minimumFractionDigits?, maximumFractionDigits? – bounded decimals (decimals fixes both; Vue's decimalPlaces is accepted)

  • signDisplay?: 'auto' | 'always' | 'exceptZero' | 'never'

  • negative?: 'minus' | 'parentheses' – 'parentheses' shows (1,234.00), as in accounting

  • percentBase?: 'fraction' | 'whole' – percent input 0.25 (default) or 25 for 25%

  • display?: 'number' | 'bar', max?: number – a progress bar beside the value, full at max (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-short

  • localized-medium

  • localized-long

  • month-name-long

  • month-year

  • dmy-numeric

  • dmy-short

  • mdy-numeric

  • mdy-short

  • iso-date

  • iso – full ISO instant

  • date, short, long – medium, short and full localized dates

  • dateTime – date and time

  • time – time only

  • relative – 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
},
ActionReceives, besides { tableId, tableType, columnId }Without it
listnothingThe column's static options
createname, color?; answers the new tagNo "Create “name”"
updateid, name?, color? (null clears it)No rename or recolor
mergesourceIds, targetIdNo merge
removeidNo 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 (unless tags: { 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 allowBulkEdit and bulkUpdate or update): the selected rows change at once; rows that fail are restored and stay selected. By default bulkUpdate(ids, { [field]: tagIds }) receives each group of rows' resulting lists; with tags: { bulk: "patch" } it receives { [field]: { add, remove } } once, for your server to apply with applyTagPatch().

  • Manage tags in the column menu renames, recolors, merges and deletes tags; a deletion states how many records use the tag (one aggregate call grouped by the column). table.canManageTags: false or tags: { manage: false } hide it.

  • Colors are palette names (TAG_COLOR_NAMES) or any CSS color; a tag without a color keeps its automatic hue, and coloredTags: false shows neutral tags.

  • Facets: a tags column in table.facets lists the catalog's names, counts each tag once per record and filters with contains.

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 select column is virtual and does not need a definition entry.

  • Add 'select' to columns.order and to columns.visible to show it when you provide an explicit visible list.

  • Controlled by table.enableRowSelection (default true).

  • Rendered as a narrow fixed-width column with a centered checkbox; same width as the actions column.

  • To hide it, remove 'select' from columns.visible.

  • It is not toggleable from the options menu UI.

Actions column (actions)

  • Add { id: 'actions', type: 'actions', header: '...' } to columns.definitions (the header label is not shown in the UI; use header: '' 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' from columns.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 typeCellInline / generated formFilter
text, stringTextText inputText operators
codeMonospace textMultiline editorText operators
numberFormatted numberNumeric inputNumeric comparisons and range
booleanBoolean indicatorBoolean controlTrue/false choices
dateDate presetCalendar date inputCalendar comparisons and range
urlLinkURL inputText operators
imageImage with fallbackImage URL inputText operators
jsonStructured valueJSON editorText operators
locationPlace label, else address, else coordinatesAddress search and coordinatesEmpty, within N km, within an area
select, tagOption labelSingle selectionMembership operators
multiSelectOption labelsMultiple selectionAny/all/none membership
dynamicTypeResolved from the row's typeKeyResolved from that same row typeText operators across mixed types
customApplication rendererExplicit catalogue field or inline editorText operators or application filter
actionsRow actionsNo data editorNo 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.

Expand ↗

Sample data. Changes stay in this preview.

Expand ↗

Sample data. Changes stay in this preview.

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.

Expand ↗

Sample data. Changes stay in this preview.

Expand ↗

Sample data. Changes stay in this preview.

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.