This guide covers changes to the beta DataTable component. These APIs may continue to evolve before the stable release.
Overview
Theselect and multiselect edit modes have been updated to use SelectMenuSync and MultiSelectMenuSync internally, replacing the previous Menu and Popover+SearchField+ListView implementations.
The options shape and onChange signature for both modes have changed to align with how SelectMenu and MultiSelectMenu work elsewhere in the design system.
Design Rationale
Why replace Menu and Popover with SelectMenu?
The previous implementations used basic primitives that lacked features available in theSelectMenu and MultiSelectMenu components:
- No built-in search field in the select dropdown
- No support for disabled options
- Inconsistent keyboard navigation compared to other select experiences in the product
- Separate codepaths to maintain for the same conceptual UI
SelectMenuSync and MultiSelectMenuSync, the DataTable’s select cells gain all of these features automatically, and the behavior is now consistent with select inputs elsewhere.
Why option objects instead of raw primitives?
The previousoptions shape used { value, label } where value was typed as the column’s data type. This created a tight coupling between options and row data types, and required extra mapping logic in onChange.
The new shape uses { id, label } — the same shape as SelectMenuOption and MultiSelectMenuOption — which aligns with how options are represented across the design system and eliminates the need for type gymnastics.
Migration Guide
Select edit mode
Before:Multiselect edit mode
Before:Live selection in multiselect
The previous multiselect cell buffered selections and only committed on Tab or F2. The new implementation callsonChange live with each selection toggle, matching how MultiSelectField works in forms. Pressing Escape closes the popover without reverting — the last onChange call reflects the committed state.
Breaking Changes
The following changes are breaking:SelectEditConfig.options— Changed from{ value: T[K]; label: string }[]toSelectMenuOption[](usesidinstead ofvalue)SelectEditConfig.onChange— Changed from(value: T[K], rowId: string) => voidto(option: SelectMenuOption | null, rowId: string) => voidMultiselectEditConfig.options— Changed from{ value: ArrayElement<T[K]>; label: string }[]toMultiSelectMenuOption[](usesidinstead ofvalue)MultiselectEditConfig.onChange— Changed from(value: T[K], rowId: string) => voidto(options: MultiSelectMenuOption[], rowId: string) => void- Multiselect commit behavior —
onChangeis now called live on each selection toggle rather than only on Tab or F2
Why breaking?
Since DataTable is a beta component, we chose to align the API with the rest of the design system rather than maintain backward compatibility. The option object shape ({ id, label }) is consistent with every other select experience in Anvil2, and the onChange signatures match those of SelectMenu and MultiSelectMenu.
Menu Customization
Bothselect and multiselect edit configs accept most props from SelectMenuSync and MultiSelectMenuSync to customize the underlying menu behavior. The only excluded props are those the DataTable cell manages itself (trigger, label, value, onSelectedOptionChange / onSelectedOptionsChange, options, onMenuKeyDown, onImplicitClose, onExplicitClose).
Common customizations:
mode: "multiselect", the selectAll and selectFiltered props are also available to add bulk-selection controls:
displayMenuAs: "auto" (dialog on mobile, popover on desktop) and width: 320.
Async Select and Multiselect
Two new async edit modes are available:"select-async" and "multiselect-async". These use SelectMenu and MultiSelectMenu directly (not the Sync variants) and accept a loadOptions function instead of a static options array.
When to use async modes
- Options are fetched from an API rather than known at render time
- Different rows need different option sets (per-row async loading)
- Option lists are large enough to benefit from server-side filtering
loadOptions and row context
The loadOptions function receives a context argument as its last parameter containing { row, rowId }. This enables per-row option loading without any additional API surface:
Lazy variants
All fourSelectMenu lazy variants are supported. Set lazy to choose the pagination strategy:
lazy value | Loader signature |
|---|---|
false (default) | (search, context) |
"page" | (search, pageNumber, pageSize, context) |
"offset" | (search, offset, limit, context) |
"group" | (search, previousGroupKey, context) |
Cache behavior
By default, caching is disabled (cache={{ enabled: false }}) to prevent cross-row cache pollution when loadOptions uses row context. To enable caching (for column-level loaders that don’t depend on row data), pass cache explicitly:
Multiselect async value contract
Unlikemode: "multiselect" (which stores an array of IDs), mode: "multiselect-async" requires the cell value to be MultiSelectMenuOption[] — full option objects. The onChange callback receives the full objects and the consumer stores them wholesale:
New Exports
The following types are now used by these edit modes and are exported from@servicetitan/anvil2/beta:
SelectMenuOption— Option type formode: "select"andmode: "select-async"columnsMultiSelectMenuOption— Option type formode: "multiselect"andmode: "multiselect-async"columnsAsyncCellContext— Context type passed toloadOptionsin async edit configs ({ row: T; rowId: string })SelectAsyncCellEagerLoader— Loader type formode: "select-async"withlazy: falseSelectAsyncCellPageLoader— Loader type formode: "select-async"withlazy: "page"SelectAsyncCellOffsetLoader— Loader type formode: "select-async"withlazy: "offset"SelectAsyncCellGroupLoader— Loader type formode: "select-async"withlazy: "group"MultiselectAsyncCellEagerLoader— Loader type formode: "multiselect-async"withlazy: falseMultiselectAsyncCellPageLoader— Loader type formode: "multiselect-async"withlazy: "page"MultiselectAsyncCellOffsetLoader— Loader type formode: "multiselect-async"withlazy: "offset"MultiselectAsyncCellGroupLoader— Loader type formode: "multiselect-async"withlazy: "group"