Customize the UI
The built-in renderer is polished by default and still has a deliberate escape ladder. Start with tokens, then use class operations or semantic parts, then regions or catalogs, and choose a custom renderer only when the application owns the complete Table structure. Customization is data passed to React—not a global mutable registry.
Choose the stylesheet
Section titled “Choose the stylesheet”Most applications should import the complete maintained default once:
import '@inertiax/react/styles.css';It is compiled and scoped. It does not need Tailwind in the application or a package-source scan.
The maintained stylesheet includes InertiaX-owned selects for Boolean, option, multi-option, and page-size choices; calendar controls for date and local datetime filters; a shared 24-hour time field for time and datetime; deliberate focus rings; and copy-action tooltips.
These are renderer defaults, not protocol requirements. Date values remain YYYY-MM-DD, local
datetime values remain YYYY-MM-DDTHH:mm[:ss[.fraction]] with one to six fractional digits, and
select values retain their original string, number, or Boolean type. The time field accepts
HH:mm[:ss[.fraction]]; incomplete typing stays local, and a date-only datetime draft does not emit
until its time is complete. Laravel keeps the accepted value exact across the request round trip,
including the T separator and omitted seconds. Basic filters still apply through the Table session’s
debounce; advanced
filters remain a draft until Apply filters, and ranges retain their explicit apply action.
For a fully independent visual treatment, import only the functional foundation:
import '@inertiax/react/base.css';With base.css, the application owns the complete visual treatment. InertiaX retains only
accessibility helpers, focus visibility, table overflow, portal safety, and reduced-motion behavior.
The application must style the documented semantic parts and all visible states itself. Do not
import both files: styles.css already includes the base layer.
Choose the scope
Section titled “Choose the scope”Application-wide customization belongs on the provider:
<InertiaXProvider table={{ texts: { refresh: { label: 'Reload' } }, theme: { classNames: { root: { add: 'my-table' } } }, }}> {children}</InertiaXProvider>One-Table customization belongs on the renderer:
<InertiaX prop="users" table={{ texts: { empty: { label: 'No team members yet.' } } }}/>Resolution is built-in → provider → component. Inputs are snapshotted, so component operations do not mutate a sibling Table or another provider root.
Theme tokens
Section titled “Theme tokens”Tokens are the first choice for a cohesive visual change. They affect built-in regions and the Table-owned columns popover without changing markup or behavior:
const applicationTable = { theme: { tokens: { primary: '#166534', ring: '#16a34a', controlRadius: '0.4rem', tableRadius: '0.85rem', cellPaddingY: '0.7rem', }, },} satisfies TableCustomizationOptions;
<InertiaXProvider table={applicationTable}>{children}</InertiaXProvider>;A component token replaces the provider value for that key and inherits every unspecified key:
<InertiaX prop="users" table={{ theme: { tokens: { primary: '#1d4ed8' } } }} />The finite token set covers surfaces, foregrounds, accent/primary/destructive states, borders, focus ring, radii, control height, cell spacing, header/row/state/skeleton colors, font, and shadow.
Keep dynamic customization stable when it matters
Section titled “Keep dynamic customization stable when it matters”The renderer uses ordinary React prop identity. Inline objects and callbacks are valid and always produce correct output, but a new reference tells React that the customization changed. If the page also owns unrelated local state, keep static customization at module scope:
const usersTable = { texts: { empty: { label: 'No team members yet.' } },};
<InertiaX prop="users" table={usersTable} />;For customization derived from state, use React’s normal useMemo and useCallback tools at the
page boundary:
const openUser = useCallback((id: string | number) => router.visit(`/users/${id}`), []);const table = useMemo( () => ({ regions: { beforeToolbar: TeamSummary }, events: { selectionChange: ({ selection }) => openUser(selection[0]!) }, }), [openUser],);
<InertiaX prop="users" table={table} />;This is an optional render optimization, not a correctness requirement. Keep a fresh object or callback when it intentionally captures fresh values.
Theme class operations
Section titled “Theme class operations”Every finite Table region has a theme key, including root, toolbar, filters, tableWrapper,
table, headers, body rows/cells, state rows, footer, pagination, selectionSummary, cell,
and filterGroup.
Use a string to append classes, or an explicit operation:
table={{ theme: { classNames: { root: { add: 'dashboard-table', remove: 'space-y-4' }, tableWrapper: { replace: 'overflow-hidden rounded-xl border' }, tableBodyRow: 'hover:bg-neutral-50', }, },}}addappends classes.removeremoves one class or a list.replacereplaces the complete class value for that key.
Use replace sparingly: it assumes responsibility for all layout behavior on that region.
Semantic CSS parts
Section titled “Semantic CSS parts”Use data-inertiax-part when a visual decision is more precise than a token or named class region.
Scope selectors beneath a class you add to this Table:
const auditTable = { theme: { classNames: { root: { add: 'audit-table' } } },} satisfies TableCustomizationOptions;
<InertiaX prop="audit" table={auditTable} />;.audit-table [data-inertiax-part='table-header-cell'] { text-transform: uppercase; letter-spacing: 0.04em;}
.audit-table [data-inertiax-part='table-row'][data-state='selected'] { background: color-mix(in oklab, var(--inx-table-primary) 12%, transparent);}data-inertiax-part is the stable styling vocabulary. data-slot, generated utility classes, and
the internal DOM depth are implementation details. A portaled part such as columns-popover is not
a descendant of the Table root, so target that documented part directly when an alternate skin
needs rules beyond the propagated tokens.
Text overrides
Section titled “Text overrides”Override only the messages your application owns:
table={{ texts: { search: { placeholder: 'Search team members…' }, empty: { label: 'No matching team members.' }, pagination: { pageSummary: '{page} / {lastPage}' }, boolean: { true: 'Enabled', false: 'Disabled' }, },}}Text groups cover search, columns, sorting, filters, Clause labels, booleans, refresh, loading,
empty/error states, selection, pagination, and copying. Placeholders such as {page}, {lastPage},
{selected}, {total}, and {column} are resolved by the built-in UI where applicable.
Structural regions
Section titled “Structural regions”Replace a bounded part of the standard UI without rebuilding the Table:
function EmptyState({ colSpan }: TableStateRegionProps) { return ( <tr> <td colSpan={colSpan}>Invite the first team member.</td> </tr> );}
<InertiaX prop="users" table={{ regions: { beforeToolbar: TeamSummary, emptyState: EmptyState, pagination: CompactPagination, }, }}/>Available regions are beforeToolbar, toolbar, afterToolbar, tableBodyWrapper,
loadingState, errorState, emptyState, footer, and pagination. Region props expose the
component definition, controller, canonical state, request status, and TanStack Table projection.
Cell, icon, and filter input renderers
Section titled “Cell, icon, and filter input renderers”Register namespaced server types through catalog operations:
<InertiaX prop="users" table={{ cellRenderers: [ { operation: 'register', key: 'acme/role', value: RoleCell }, ], filterInputs: [ { operation: 'register', key: 'acme/role-filter', value: RoleFilterInput }, ], }}/>Use register for a new key and replace only for an existing key. Duplicate registration and a
missing replacement fail with context. Built-in keys are stable product behavior; prefer a
namespaced custom type to globally replacing a built-in unless you intentionally own that change.
See Custom types for the matching Laravel half.
The same catalog can replace a built-in control when native behavior or an application design system is preferable. This example deliberately takes ownership of the string-select value:
import type { TableFilterInputRendererProps } from '@inertiax/react';
function NativeSelectInput({ ariaLabel, definition, id, onChange, value }: TableFilterInputRendererProps) { return ( <select aria-label={ariaLabel} id={id} onChange={(event) => onChange(event.target.value || undefined)} value={typeof value === 'string' ? value : ''} > <option value="">Any</option> {definition.options?.map((option) => ( <option key={String(option.value)} value={String(option.value)}> {option.label} </option> ))} </select> );}
<InertiaX prop="users" table={{ filterInputs: [ { operation: 'replace', key: 'select', value: NativeSelectInput }, ], }}/>For non-string options, the replacement must preserve the declared JSON primitive identity just as the built-in control does. Replace one catalog leaf for a control family; use a region or complete renderer only when the application owns a larger interaction boundary.
Complete renderer ownership
Section titled “Complete renderer ownership”Custom children are the last step when regions and leaf catalogs are no longer enough. They retain the same Table session, controller, state, status, and request lifecycle, but the application owns all rendered structure:
<InertiaX prop="users"> {(session) => <UsersGrid session={session} />}</InertiaX>Use this for a genuinely different renderer, not to change spacing or replace one cell.
Package a reusable skin
Section titled “Package a reusable skin”A reusable skin is compiled CSS plus an ordinary TableCustomizationOptions preset. Export the CSS
and the stable preset from your own design-system package, import @inertiax/react/base.css once,
and pass the preset at provider or component scope. There is no runtime skin registry and no hidden
plugin lifecycle.
When CSS is enough
Section titled “When CSS is enough”Use styles.css plus tokens and theme class operations for color, spacing, borders, and typography.
Use semantic parts for precise CSS, structural regions when markup or behavior must change, and a
catalog renderer when one protocol type needs new content. This keeps customization local and
avoids replacing the entire Table for a small visual difference.