# React customization API

> Renderer options, theme tokens, text keys, regions, and renderer contracts.

Source: https://docs.inertiax.dev/table/reference/react/

`TableCustomizationOptions` and the types below are exported by `@inertiax/react-table`. Pass customization options to `<TableProvider customization={...}>` or `<AppTable customization={...} />`. Resolution is built-in → provider → component, isolated from sibling tables.

## Component options

| Option          | Purpose                                                                             |
| --------------- | ----------------------------------------------------------------------------------- |
| `prop`          | Named host prop; required by `Table` and your app’s `AppTable` binding              |
| `host`          | Structural query service; required by `Table`, supplied by `AppTable`               |
| `envelope`      | Supplied response; required by the direct `InertiaXRenderer`                        |
| `debounceMs`    | Shared search/filter delay, default 300 ms; nonnegative; `0` dispatches immediately |
| `controllerRef` | Ref to the table controller; cleared on unmount                                     |
| `customization` | Theme, text, regions, renderers, and event handlers                                 |
| `pipeline`      | Advanced transformations of validated envelopes                                     |
| `children`      | React content or a function receiving the session; replaces the default table UI    |

See [Direct envelope](https://docs.inertiax.dev/table/guide/direct-rendering/) for the supplied-envelope workflow and ownership rules.

## Pinned filters

The built-in toolbar and [`TableFilters`](https://docs.inertiax.dev/table/reference/react/#layout-controls) show the filters marked `pinned()` in Laravel. `customization.pinnedFilters?: readonly string[]` replaces that list with filter keys: a component list replaces the provider list, and `[]` pins nothing. Missing keys, blank keys, and duplicates report `INERTIAX_REACT_TABLE_CUSTOMIZATION_INVALID`.

A pinned filter shows a select when its `equals` clause takes one `select` value. Pinned editing supports Basic mode and Equals conditions in a flat AND expression. Choosing an option replaces the existing condition in place, or appends one when absent. Clearing removes only that condition. Each change uses the normal filter request delay and resets the page; applied chips and restored URL state use the same filter state.

Number `0`, string `"0"`, and Boolean `false` remain distinct. A filter without such a clause shows **Edit filters** instead, as do `multiple()` selects, repeated conditions, other comparisons such as **Is empty**, unavailable option values, and Advanced mode. The action opens the regular filter controls; Basic conditions remain editable through their chips. Pinning does not simplify or rewrite these expressions.

A custom `toolbar` region owns placement: built-in pinned controls are omitted, while the regular filter control remains available below the replacement. Pinned controls use the same `filterInputs`, option labels, and translated texts as the filter editor. A custom input emits `undefined` to clear that condition.

## Theme tokens

Set `theme.tokens` to a partial token object. Unknown token keys fail validation.

| Group                | Token keys                                                                                                                                                                                                                     |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Surfaces and color   | `background`, `foreground`, `surface`, `surfaceForeground`, `popover`, `popoverForeground`, `muted`, `mutedForeground`, `accent`, `accentForeground`, `primary`, `primaryForeground`, `destructive`, `border`, `input`, `ring` |
| Geometry and spacing | `controlRadius`, `tableRadius`, `controlHeight`, `cellPaddingX`, `cellPaddingY`                                                                                                                                                |
| Table states         | `headerBackground`, `rowHoverBackground`, `rowSelectedBackground`, `skeletonBackground`, `stateBackground`                                                                                                                     |
| Type and elevation   | `fontFamily`, `fontSize`, `shadow`                                                                                                                                                                                             |

## Class names

Set `theme.classNames` using these targets:

| Area                   | Theme keys                                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Outer controls         | `root`, `toolbar`, `filters`, `footer`, `pagination`, `selectionSummary`, `filterGroup`                                   |
| Table structure        | `tableWrapper`, `table`, `tableHeader`, `tableHeaderRow`, `tableHeaderCell`, `tableBody`, `tableBodyRow`, `tableBodyCell` |
| State and cell content | `stateRow`, `stateCell`, `cell`                                                                                           |

A string appends classes. An object accepts `add: string`, `remove: string | readonly string[]`, and `replace: string`. Replacement supplies the whole class value for that target.

## Text overrides

Each group is partial except `clauses`, which is a comparison-key-to-label map.

| Group                         | Keys                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `search`                      | `label`, `placeholder`, `clear`                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `columns`                     | `label`, `searchPlaceholder`, `empty`, `toggle`                                                                                                                                                                                                                                                                                                                                                                                                          |
| `sorting`                     | `ascending`, `descending`, `label`, `none`                                                                                                                                                                                                                                                                                                                                                                                                               |
| `filters`                     | `label`, `add`, `addGroup`, `clear`, `apply`, `applyRange`, `and`, `or`, `any`, `match`, `conditions`, `groupOperator`, `field`, `clause`, `remove`, `removeGroup`, `empty`, `rangeFrom`, `rangeTo`, `valuePlaceholder`, `optionsTruncated`, `editFilters`, `clearPinned`, `addTitle`, `editTitle`, `save`, `cancel`, `close`, `value`, `searchFields`, `noFields`, `activeCount`, `more`, `less`, `editDescription`, `removeDescription`, `invalidated` |
| `boolean`                     | `true`, `false`; filter choices and fallback cell labels                                                                                                                                                                                                                                                                                                                                                                                                 |
| `refresh`, `loading`, `empty` | `label`                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `error`                       | `label`, `retry`                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `problem`                     | See [problem wording](https://docs.inertiax.dev/table/reference/react/#problem-outcomes)                                                                                                                                                                                                                                                                                                                                                                 |
| `selection`                   | `summary`, `all`, `row`                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `result`                      | `summary`                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `pagination`                  | `rowsPerPage`, `pageSummary`, `first`, `previous`, `next`, `last`                                                                                                                                                                                                                                                                                                                                                                                        |
| `copy`                        | `label`, `success`, `error`                                                                                                                                                                                                                                                                                                                                                                                                                              |

Server-supplied clause and Boolean cell labels take precedence over `texts.clauses` and `texts.boolean`. See [Labels from Laravel](https://docs.inertiax.dev/table/guide/text/#labels-from-laravel) for translation.

Filter text templates use these placeholders:

| Keys in `texts.filters`                | Placeholder                                                    |
| -------------------------------------- | -------------------------------------------------------------- |
| `activeCount`, `more`                  | `{count}`: number of active or hidden conditions, respectively |
| `editDescription`, `removeDescription` | `{filter}`: full condition description                         |
| `rangeFrom`, `rangeTo`, `clearPinned`  | `{filter}`: field label                                        |

`applyRange` is retained for source compatibility but is unused by the built-in editor.

## Regions

Result regions receive `component`, `controller`, `state`, `status`, and `table` (the TanStack table). The separate `problemState` region has no successful result or TanStack table.

| Region                                                             | Prop type and additions                                                                 |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| `beforeToolbar`, `toolbar`, `afterToolbar`, `footer`, `pagination` | `TableRegionProps`                                                                      |
| `tableBodyWrapper`                                                 | `TableBodyWrapperRegionProps`, plus `children`                                          |
| `loadingState`, `errorState`, `emptyState`                         | `TableStateRegionProps`, plus `colSpan`                                                 |
| `problemState`                                                     | `TableProblemRegionProps`: `componentId`, `problem`, `controller`, `status`, `canReset` |

A custom `toolbar` region keeps the built-in filter control below it and the `afterToolbar` region.

## Layout controls

These components render the table’s search, filter, sort, and pagination controls in custom children of `<AppTable>`, `Table`, `InertiaXRenderer`, or `TableRenderer` ([example](https://docs.inertiax.dev/table/guide/custom-layouts/#add-search-filters-sorting-and-pagination)). `TableSort` is a sort select in place of the built-in table’s header buttons. Each renders nothing for a [problem outcome](https://docs.inertiax.dev/table/reference/react/#problem-outcomes), or when the table doesn’t offer its feature:

| Component         | Renders                                                                                | Renders nothing when                     |
| ----------------- | -------------------------------------------------------------------------------------- | ---------------------------------------- |
| `TableSearch`     | The search box                                                                         | No column is searchable                  |
| `TableFilters`    | The filter trigger, pinned filters, and applied conditions, in the table’s filter mode | The table has no filters                 |
| `TableSort`       | A select of the sortable columns, and a direction button for the chosen one            | Sorting is off, or no column is sortable |
| `TablePagination` | The page size, page summary, and page buttons                                          | Pagination is off                        |

`TableSort` sets a single sort key, even when the table allows several, and a new column starts ascending; it has no option that removes the sort. `texts.sorting.label` names its select, and `texts.sorting.none` shows in it when the table has no sort. `TableFilters` accepts an optional `children` function, `TableFilterLayout`, which receives `trigger`, `pinned` (`null` when no filter is pinned), and `applied` to place them yourself. The `pagination` region applies only to the built-in table.

## Cell, icon, and input renderers

| Customization array | Renderer props                                                                                               |
| ------------------- | ------------------------------------------------------------------------------------------------------------ |
| `cellRenderers`     | `TableCellRendererProps`: `cell`, `column`, and `rowId`                                                      |
| `iconRenderers`     | `TableIconRendererProps`: cell renderer props plus `icon`                                                    |
| `filterInputs`      | `TableFilterInputRendererProps`: `ariaLabel`, `clause`, `definition`, optional `id`, `value`, and `onChange` |

Use `register(key, value)` for a new type or `replace(key, value)` for an existing type. They return `{ operation: 'register', key, value }` and `{ operation: 'replace', key, value }`, which you can also write directly. Duplicate registration and missing replacement targets fail. See [Registration & diagnostics](https://docs.inertiax.dev/table/guide/registration/).

Filter inputs are registered by value type. The editor draws the input for `clause.input.valueType`: none for a clause without `input`, two for a `range` (its from and to values), and one otherwise. Built-in types are `text`, `number`, `boolean`, `select`, `date`, `datetime`, and `time`; replacing `select` also replaces the pinned select. A custom input reads `clause.input.mode` and, for `select`, `clause.input.options`.

Custom filter inputs preserve JSON primitive types and emit `undefined` to clear the draft value. The editor commits with Add/Save (Basic) or Apply (Advanced). Inputs mount while editing, and a `select` input also mounts for a pinned filter. A native select emits strings: converting numeric or Boolean options to strings changes their identity. Decode them before calling `onChange`, and emit an array for a `multiple` input.

## Filter value summaries

`customization.formatFilterValue(props: TableFilterValueFormatterProps): string | undefined` formats the value portion of an applied Basic filter. Props contain readonly `definition`, `clause`, and `value`. The callback receives the entire value, including both endpoints for a range or every value of a list. It is not called for clauses without `input`.

Returning `undefined` delegates from component to provider to the built-in formatter. Only `undefined` delegates; an empty string hides the value portion of the chip. Built-in formatting uses the labels of `clause.input.options`, translated Boolean labels and unchanged date/time strings. It quotes `text` values, joins range endpoints with a dash and separates list values with commas. Object values use JSON text.

Formatting does not change the submitted value. Invalid configuration, invalid return types (including `null`) and callback failures report `INERTIAX_REACT_TABLE_CUSTOMIZATION_INVALID`. See [Format applied values](https://docs.inertiax.dev/table/guide/custom-filters/#format-applied-values) for an example.

## Filter appearance

Use these `data-inertiax-part` values to style the built-in filter controls:

| Area                  | Parts                                                                                                                              |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Trigger and summaries | `filters-trigger`, `filters-summary`, `filters-overflow`                                                                           |
| Applied Basic filters | `filters`, `filter-chip`, `filter-chip-edit`, `filter-chip-remove`, `filter-chip-field`, `filter-chip-clause`, `filter-chip-value` |
| Pinned controls       | `pinned-filters`, `pinned-filter`, `pinned-filter-controls`, `pinned-filter-clear`, `pinned-filter-edit`                           |
| Field picker          | `filter-picker`, `filter-picker-search`, `filter-picker-list`, `filter-picker-item`                                                |
| Editor                | `filter-editor`, `filter-editor-header`, `filter-editor-body`, `filter-actions`, `filters-apply`, `filters-clear`, `filters-close` |
| Conditions            | `filter-group`, `filter-condition`, `filter-field`, `filter-clause`, `filter-input`, `filter-range`                                |

Editor portals carry `data-inertiax-table-portal` with the table’s ID and inherit its theme tokens. To target one table’s editor:

```css
[data-inertiax-table-portal='users'][data-inertiax-part='filter-editor'] {
  border-radius: 0.75rem;
}
```

Desktop editors use popovers; screens up to 640px wide use a modal panel, except inside a dialog (see [Tables in dialogs](https://docs.inertiax.dev/table/reference/react/#tables-in-dialogs)). The `filter-editor` part applies to both. Advanced filters use `filters` inside the editor instead of an applied-chip strip.

Ranges and valueless conditions use the editor’s Add/Save or Apply action.

## Tables in dialogs

A table inside a dialog, such as a modal, drawer or sheet (shadcn’s Dialog or Sheet, a `<dialog>` element, or any element with `role="dialog"` or `role="alertdialog"`), renders its popups inside that dialog, so they work with its focus trap. They stay within the nearest transformed panel around the table, else within the dialog; when the dialog element has no size of its own (as in Headless UI) and no such panel exists, they stay within the screen. The filter editor is a popover there on every screen.

In Radix dialogs (including shadcn’s Radix components), Headless UI and `<dialog>`, Escape closes the open table popup first, and a second Escape closes the dialog. MUI, React Aria and Base UI dialogs (including shadcn’s Base UI components) close on every Escape. A Radix dialog’s `onEscapeKeyDown` still runs for the first Escape; check `event.defaultPrevented` there before acting on it.

The table looks for the dialog around the whole table: in a custom layout, a filter control placed in a different dialog keeps its popups outside that dialog. An app’s own Radix popup inside a custom filter input is a separate copy of Radix, so Escape there also closes the filter editor. The popups stack within the dialog’s content: if an element around the table starts its own stacking context, for example a positioned element with `z-index` or one with `isolation: isolate`, positioned content after it in the dialog can cover them. Give that element a `z-index` above that content, or remove its stacking context.

## React prop identity

Inline customization objects are valid. Keep static presets at module scope; for state-dependent presets, `useMemo` and `useCallback` can avoid rebuilding customization on unrelated renders. This is an optional optimization, not a correctness requirement, except for `pipeline`: see [Envelope pipelines](https://docs.inertiax.dev/table/reference/react/#envelope-pipelines). Closures must still capture the current values they use.

Event payloads and controller methods are listed in [Controller & events](https://docs.inertiax.dev/table/guide/controller-events/).

## Envelope pipelines

| Helper                                        | Operation                                                                         |
| --------------------------------------------- | --------------------------------------------------------------------------------- |
| `addStage(name, transform, { priority })`     | Adds a stage; the name must be new                                                |
| `replaceStage(name, transform, { priority })` | Replaces a stage by name; without `priority`, keeps the replaced stage’s priority |
| `removeStage(name)`                           | Removes a stage by name                                                           |

Each helper returns the matching `{ operation: 'add' | 'replace' | 'remove', ... }` object, which you can also write directly. Stages run in ascending `priority`, default 10; at equal priority, provider stages run before component stages, in declaration order. A replacement runs where it is declared, so a component’s `replaceStage()` of a provider stage runs after the provider’s other stages of equal priority.

A stage receives `TableEnvelope`, the union of successful `Envelope` and `ProblemEnvelope`, as a read-only value and returns it or a new envelope built from it. Use `isProblemEnvelope()` from `@inertiax/table-protocol` before accessing result fields; a pipeline that only changes columns should return problem envelopes unchanged.

Keep `pipeline` arrays at module scope or memoize them: a new array recreates the sessions under that provider or component and loses their client state. Type the array with `satisfies TableProps['pipeline']` so `addStage()` infers the envelope type. See [Envelope pipelines](https://docs.inertiax.dev/table/guide/registration/#envelope-pipelines) for an example.

## Session lifecycle

Table APIs and CSS are exported by `@inertiax/react-table`. `useTableProviderRuntime()` reads the runtime belonging to the nearest `TableProvider`; `useTableRuntime()` reads the current Table renderer’s component scope.

Foundation `@inertiax/react` supplies a separate `InertiaXProvider` for app-owned services. It does not create a Table runtime or replace `TableProvider`. An application binding can read `useInertiaXServices()` and pass its optional `floatingRoot` to `TableProvider` explicitly.

For a caller-owned runtime, use `createTableRuntime()` from `@inertiax/table-core`. Pass it to `TableProvider runtime={runtime}` or `createTableSession({ runtime, ... })`. The provider creates one automatically when omitted. Generic Core runtimes cannot be supplied to Table; Table owns its catalogs, pipelines, diagnostics, and their `TableRuntimeError` class.

Replacing a supplied runtime preserves valid table state when the component ID and integration are unchanged. The caller owns both runtimes and must dispose them when no longer used. A different component ID or integration starts separate state.

A session passed to a `children` render function can be replaced after Activity or Suspense restores the table. Use the session passed to the current render; do not save it for later use. For custom controls, prefer [controller hooks](https://docs.inertiax.dev/table/guide/controller-events/#read-reactive-state).

If you create sessions yourself, `createTableSession()` from `@inertiax/table-core` accepts an optional `initialState` containing a complete `TableState`:

```ts
import { createTableSession } from '@inertiax/table-core';

const session = createTableSession({
  componentId: 'users',
  integration,
  initialState: savedState,
});
```

The state is validated against the current table definition and copied. If the integration supplies a location, that location takes precedence for search, filters, sorting, and pagination; missing parameters use defaults from the supplied table response.

Supplying `initialState` starts no request. Call `session.controller.refresh()` if the supplied rows do not match the restored state. Without `initialState`, a location that differs from the supplied response triggers a request automatically. Dispose a caller-owned session when finished; a disposed session cannot be reused.

## Problem outcomes

`useTableSnapshot()` returns an `outcome: 'success' | 'problem'` union. A successful snapshot has a complete envelope and `TableState`. A problem snapshot has `state: null`, a problem envelope, and `canReset`. `useTableState()`, `useTableEnvelope()`, and TanStack projection require a successful outcome. Custom renderers must branch before mounting components that call those hooks.

The built-in renderer uses `customization.regions.problemState` for invalid external settings and `customization.texts.problem` for its `label`, `reset`, `unavailable`, `page`, `pageSize`, `sorting`, `filters`, and `search` wording. See [Recover table links](https://docs.inertiax.dev/table/guide/url-recovery/).
