Skip to content

RFC: TanStack Select concept inventory and rewrite direction #30

Description

@tannerlinsley

TanStack Select RFC: concept inventory and rewrite direction

Status: Reconciled draft

Scope: Product and architecture direction, not a final API

Source: The current implementation, its history, repository feedback, and Kevin Van Cott's initial architecture assessment

Summary

TanStack Select should be a headless, framework-agnostic engine for building accessible, collection-based selection inputs.

A sufficient select is a small data grid. It needs stable item identity, derived models, filtering and ranking, grouping, selection models, keyboard navigation, async data, virtualization, and controlled state. Table solves many of the data problems that make selects powerful; it solves almost none of the accessibility and focus problems that make selects hard. The rewrite should use the same broad separation that makes TanStack Table work:

  1. User data becomes a stable option model.
  2. Feature stages derive the options available for display and interaction.
  3. State and actions operate on option IDs, not array positions or DOM nodes.
  4. A framework-agnostic core owns data and behavior.
  5. Framework adapters make the core reactive and expose accessible prop getters.
  6. Rendering, markup, styling, positioning, and virtualization remain replaceable.

This is not a proposal to turn Select into a styled component library or to make Table a dependency. It is a proposal to give Select a similarly capable collection core and a purpose-built interaction layer.

Product contract

The package should make simple selects easy without limiting complex ones. Its irreducible responsibilities are:

  • Model arbitrary user data as identifiable, selectable options.
  • Derive the options that are searchable, available, visible, and navigable.
  • Keep committed selection separate from transient interaction state.
  • Support ordered single and multiple selection, including repeated values when enabled.
  • Support range selection and select-all semantics when the active feature set enables them.
  • Support local, controlled, and remote search.
  • Support creating values that are not in the current option collection.
  • Implement complete keyboard, pointer, touch, focus, and screen-reader behavior.
  • Remain usable with portals, popovers, variable layouts, and virtualized lists.
  • Let every important state slice be controlled without requiring all state to be controlled.
  • Expose enough models and actions to build product-specific interfaces without forking the engine.

Scope boundary

Stable initial scope

  • Select-only comboboxes with a button-like trigger and popup listbox.
  • Editable restricted comboboxes.
  • Free-form autocomplete and suggestions.
  • Always-visible listboxes.
  • Single and multiple selection, including tag/chip navigation and removal.
  • Disabled options and groups, typeahead, local filtering, and relevance sorting.
  • Controlled and uncontrolled state.
  • Deterministic IDs, SSR, RTL, localized announcements, forms, reset, validation, and an explicit autofill strategy.
  • Async and loading state without owning fetching.
  • A virtualization bridge without depending on TanStack Virtual.

Designed later

  • Hierarchical and tree pickers.
  • Grid popups for rich structured options.

Out of scope

  • Ranger and sliders. A future TanStack Slider may share infrastructure, but it is a different widget family.
  • Date, time, and color pickers; radio groups; segmented controls; and number inputs.
  • Required component anatomy, CSS, popup positioning, animation, or data fetching.

Documentation should be native-first. If a native <select> meets the product's requirements, recommend it before a custom control.

Opinions already encoded in the current library

These are the concepts to preserve. Their current names and implementations are not commitments.

Existing concept Product opinion to preserve Rewrite implication
Headless hook and prop getters Behavior must not dictate markup or styling. User event handlers must compose with library behavior. Core plus framework adapters; accessible prop getters or equivalent bindings.
Controlled value and onChange The application owns committed selection. Controlled/uncontrolled state slices with typed change metadata.
Internal searchValue, isOpen, and highlightedIndex Committed selection and interaction state are different things. Separate selection, query, open, active option, focus, and async state.
stateReducer with action types Consumers must be able to intercept and replace default transitions. Every transition has a typed action/reason and can be controlled or reduced.
multi Multiple selection is a primary mode, not an add-on. One internal ordered selection model with type-safe single/multiple adapters.
duplicates and removal by index A multi-select may be an ordered multiset, not only a set. One occurrence must be removable without removing all equal values. Selection entries need identity distinct from option value.
Hide selected options unless duplicates are allowed Availability is a derived model and selection can affect it. A configurable availability stage, not destructive filtering of source data.
Missing selected options are synthesized A selected value must survive when its option is absent, stale, remote, or not loaded yet. First-class unresolved/orphaned value resolution with explicit labels and hydration.
create and getCreateLabel User input can become a committed value, and its presentation is customizable. A typed creation candidate and lifecycle, separate from real options.
filterFn Search is replaceable and may both filter and rank. Pluggable filtering/ranking stages with metadata.
Raw and debounced resolved search Input feedback must be immediate while expensive derivation or fetching may lag. Separate input query from effective query; debounce belongs at the derivation/fetch boundary.
Debounce based on option count Large collections need a different performance strategy. Indexing, memoized models, worker/remote escape hatches, and consumer-configured debounce rather than a hard-coded heuristic.
visibleOptions Consumers need the final navigable option model, not only source data. Named option-model stages with public getters.
Index-based option actions The implementation anticipated windowing and items not all being mounted. Public identity should be ID-based; adapters may resolve visible indices for virtualizers.
scrollToIndex Active-item navigation must coordinate with virtualization. A scroll-to-option contract and official TanStack Virtual integration example.
Arrow, page, home/end, enter, escape, tab, and backspace behavior Keyboard interaction is core behavior. A tested interaction state machine following the applicable ARIA pattern.
Backspace removes the last value only when query is empty Text editing takes precedence over tag removal. Preserve as a configurable default with selection-entry focus for richer tag navigation.
Single selection closes; multiple selection stays open and clears search Commit behavior depends on selection mode. Typed close/query-reset policies keyed by action reason.
Search resets active item Query changes invalidate current navigation. Configurable active-item reconciliation by ID, with a deterministic fallback.
Outside interaction closes the popup Dismissal is part of the behavior contract. Interaction boundary may include trigger, input, popup, and portaled descendants.
Deferred blur and refocus after opening Focus must survive composite controls and portals. Explicit focus-within and dismissal semantics; no timer-based focus hacks.
Values are separate from option objects Applications should not have to store library-owned option wrappers. Generic accessors for ID, value, label, disabled state, text, and metadata.

Historical feedback worth accommodating

Repository issues also exposed needs that were not fully implemented:

  • Async option fetching.
  • TypeScript-first APIs with correct single/multiple inference.
  • Search that moves the active item without filtering the collection.
  • Optional inline completion/ghost text.
  • Examples without styling or rendering dependencies.
  • First-class accessibility rather than a future improvement.

These should enter the design backlog, but they do not have the same status as behavior already present in the library.

Concepts that should not survive literally

  • Array indices must not be durable option identity. Filtering, sorting, grouping, async updates, and virtualization all invalidate them.
  • { value, label } must not be the only accepted data shape.
  • A creatable value must not masquerade as a normal option. That creates identity collisions and cannot represent validation, pending creation, or failure.
  • Missing selected values must not silently become fake source options. Their unresolved state should be observable.
  • Filtering must not assume string values or mutate meaning by sorting with an incomplete comparator.
  • Option-count-based debounce must not be built-in policy.
  • Focus and blur correctness must not depend on setTimeout.
  • An optionsRef that represents only one DOM subtree is insufficient for portaled composite controls.
  • A single global reducer should not be the only control point. State slices and callbacks should also be individually controllable.
  • The core must not import React or the DOM.
  • Accessibility cannot be delegated entirely to consumers; the adapter must produce correct roles, relationships, IDs, and interaction handlers.

Relationship to Table v9 and the TanStack ecosystem

Select should replicate the proven shape of Table v9 without importing Table or extracting a shared generic framework prematurely:

  • A static feature object selects first-party features and preserves tree-shaking and inference.
  • Features contribute initial state, default options, instance data, models, and APIs.
  • State slices use individually subscribable TanStack Store atoms.
  • Uncontrolled state, external atoms, and controlled state share the same update machinery.
  • Feature slots declare prerequisites and produce useful type errors for invalid combinations.
  • Type maps and declaration merging avoid large conditional unions and preserve declaration performance.

The feature graph is a typed assembly mechanism, not permission for features to own arbitrary interaction behavior. Cross-cutting keyboard, focus, and ARIA invariants remain coordinated by the interaction layer. We should let Table and Select converge as separate implementations before deciding whether any feature infrastructure belongs in a shared package.

TanStack Store should be an internal dependency. Form, Table, Virtual, Pacer, Query, and Hotkeys should integrate by composition without becoming core dependencies.

The market position is not merely another Downshift. It is a multi-framework, Table-grade option engine with accessible interaction bindings, fine-grained subscriptions, inspectable models, and no required rendering or positioning system.

Option and selection models

Source option

Users should pass arbitrary objects. Accessors derive the fields Select requires.

type SelectOptions<TOption, TValue> = {
  options: TOption[]
  getOptionId: (option: TOption, index: number) => string
  getOptionValue: (option: TOption) => TValue
  getOptionLabel: (option: TOption) => string
  getOptionText?: (option: TOption) => string
  getOptionDisabled?: (option: TOption) => boolean | string
  getSubOptions?: (option: TOption) => TOption[] | undefined
}

getOptionId is the engine identity. getOptionValue is the application payload. They are deliberately different: multiple options may share a value, values may not be strings, and selected occurrences may repeat.

Derived option

The core wraps each source option with stable derived behavior and metadata, similar to a Table row.

type SelectOption<TOption, TValue> = {
  id: string
  original: TOption
  value: TValue
  label: string
  depth: number
  parentId?: string
  disabled: boolean
  disabledReason?: string
  index: number
  getIsSelected(): boolean
  getIsActive(): boolean
  getIsAvailable(): boolean
  getFilterMeta(): unknown
  select(reason?: SelectActionReason): void
}

The exact methods are open, but the model must expose stable identity, source data, derived state, and actions.

Selection entry

Committed multi-selection is an ordered list of occurrences.

type SelectionEntry<TValue> = {
  id: string
  value: TValue
  optionId?: string
}

optionId may be unresolved when a value is restored before its option is loaded. id distinguishes duplicate occurrences. Applications may continue to receive plain values in the common API; entries are the lossless internal model and advanced API.

Selection equality must be configurable with isValueEqual, with referential or Object.is semantics as a documented default. Serialization is a separate concern.

Option-model pipeline

Each stage should be memoizable, replaceable, and observable. Features may skip stages with manual* options, following familiar TanStack conventions.

source options
  -> core option model (identity, hierarchy, accessors)
  -> availability model (disabled/hidden/already selected/policy)
  -> filtered model (query and custom filters)
  -> ranked/custom-sorted model
  -> grouped model
  -> expanded hierarchy
  -> flattened visible model
  -> navigable model (non-disabled interactive options)

The public names can change. The important constraint is that “options” is not one mutable array.

Filtering and autocomplete

Support three query behaviors:

  • filter: remove non-matches and optionally rank matches.
  • navigate: keep the list intact and move the active option to the best match.
  • manual: expose query changes while the application provides the resulting options.

Filter functions should return match metadata, not only booleans, so ranking and highlighted text can reuse the same work. The core should ship a small text filter; fuzzy matching can be a separate entry point if its size is material.

The query model needs at least:

  • inputValue: what is currently in the editable input.
  • query: the value used by local derivation or remote loading.
  • completion: optional inline completion derived from the active match.

IME composition must suppress selection and filtering transitions until composition is committed.

Grouping and faceted input

Grouping belongs in the option pipeline. Faceting and aggregation usually belong upstream. For example, Table can produce faceted values and counts while Select presents them.

  • Groups may come from nested source data or a grouping function.
  • Group headers are presentational/navigational nodes, not selectable options by default.
  • Selected options remain resolvable when filtering or grouping excludes them from the visible model.
  • Select should accept count and facet metadata on source options without needing to calculate it.
  • A later faceting feature is reasonable only if independent Select use cases justify owning that derivation.

State and actions

Suggested state shape:

type SelectState<TValue> = {
  selection: SelectionEntry<TValue>[]
  inputValue: string
  query: string
  isOpen: boolean
  activeOptionId: string | null
  focusedSelectionId: string | null
  filters: SelectFilterState
  expanded: Record<string, boolean>
}

Async request status should be modeled but may live in an async feature rather than the base state.

Every update should carry metadata:

type SelectChangeMeta = {
  action:
    | 'select-option'
    | 'deselect-option'
    | 'remove-selection'
    | 'clear-selection'
    | 'create-option'
    | 'set-input-value'
    | 'set-query'
    | 'set-active-option'
    | 'open'
    | 'close'
  reason: 'keyboard' | 'pointer' | 'touch' | 'focus' | 'programmatic' | 'outside' | 'form-reset'
  optionId?: string
  selectionId?: string
  originalEvent?: unknown
}

Each state slice should support the TanStack controlled-state pattern: initial state, external state, a slice callback, and a whole-state callback. A reducer/transition hook remains useful for changing default behavior, but consumers should not need it for routine control.

Interaction contract

The interaction layer should be a state machine tested independently from every framework adapter.

Default behavior

  • Opening chooses a deterministic active option: selected option when appropriate, otherwise the first navigable option.
  • Arrow keys open and move through navigable options.
  • Home/End move to boundaries. Page Up/Down move by a configurable page calculation, not a magic fixed offset.
  • Shift-range selection extends an anchor through the navigable option model when enabled.
  • Enter commits the active option while open and never submits a form when it performed a select action.
  • Escape closes and optionally restores the pre-open input value; a second Escape may clear according to policy.
  • Tab closes without trapping focus. Whether it commits an active option is explicit policy and off by default.
  • Backspace edits text first; with an empty input in multiple mode, it focuses or removes the last selected occurrence according to policy.
  • Pointer movement may activate options without stealing DOM focus from the input.
  • Disabled options are never committed and are skipped by keyboard navigation.
  • Single commit closes by default. Multiple commit keeps the popup open and clears the query by default.
  • A query/model change reconciles the active option by ID before falling back to the first navigable match.

Accessibility

The adapter should implement and test the WAI-ARIA combobox/listbox patterns, including:

  • Stable IDs and aria-controls, aria-expanded, aria-activedescendant, and aria-selected relationships.
  • Correct single- versus multi-select listbox semantics.
  • Labels, descriptions, validation state, required state, and disabled/read-only state.
  • Live announcements for result counts, selection changes, creation, loading, and failures without double announcements.
  • Portaled listboxes while DOM focus remains on the combobox input.
  • Virtualized active options that remain represented for assistive technology.
  • High-contrast and reduced-motion compatibility in any optional UI examples.

The supported markup patterns should be explicit. A prop getter cannot guarantee accessibility if consumers attach it to an arbitrary element with an incompatible role.

Focus strategy should be an explicit first-party feature rather than one universal implementation:

  • Editable comboboxes generally keep DOM focus on the input and use aria-activedescendant.
  • Always-visible listboxes and compatible select-only patterns should be able to use real DOM focus with roving tabindex.
  • Adapters may select or constrain the strategy by widget pattern and platform because VoiceOver and Safari behavior is not uniform.

This is the largest greenfield part of the project. It needs a real assistive-technology matrix, not only ARIA attribute assertions.

Async data

Async behavior should be supported without making TanStack Query a dependency.

  • manualFiltering lets query changes drive external fetching.
  • Loading, refreshing, empty, and error are distinct states.
  • Results are associated with a query/request identity so stale responses cannot replace newer ones.
  • Aborted requests are not failures.
  • Selected unresolved values can be hydrated independently from the visible result page.
  • Pagination and infinite loading append or reconcile by option ID.
  • Cached data may remain visible while a new query loads.
  • Creation may be sync or async and needs idle, validating, pending, success, and error outcomes.

Documentation should include TanStack Query integration, but the core contract should work with any data source.

Creatable values

Creation should be a feature with an explicit candidate:

type CreateCandidate<TValue> = {
  inputValue: string
  value?: TValue
  label: string
  status: 'valid' | 'invalid' | 'pending'
  reason?: string
}

The consumer controls parsing, normalization, duplicate detection, validation, and persistence. The candidate can appear in the navigable model but cannot collide with an option ID. On success it may select a returned option/value; on failure it preserves input and exposes the error.

Virtualization and performance

  • Core operations use ID maps and memoized model stages instead of repeated linear lookups where avoidable.
  • The core never requires every option to have a mounted element.
  • Active-item APIs are ID-first. The visible model can map IDs to indices for a virtualizer.
  • Adapters expose a scroll request with option ID, current visible index, alignment, and reason.
  • Variable-height and grouped lists must work; Select should not own measurement.
  • Filtering can be deferred, indexed, moved to a worker, or made manual.
  • Benchmarks should cover 100, 10,000, and 100,000 options, query updates, selection updates, and virtualized navigation.

TanStack Virtual should be the reference integration, not a hard dependency.

Form behavior

The package should define rather than accidentally inherit form semantics:

  • Configurable value serialization for hidden inputs or form-associated adapters.
  • Multiple values represented without lossy delimiter joining.
  • Native form reset restores initial selection and interaction state.
  • Required and validation behavior works for single and multiple modes.
  • Pressing Enter only prevents form submission when Select handled the key.
  • Autofill is documented as supported, limited, or unsupported for each rendering pattern.

Package architecture

Proposed package boundaries:

@tanstack/select-core       Framework-agnostic models, features, state, and actions
@tanstack/react-select      React adapter and DOM prop getters
@tanstack/solid-select      Solid adapter
@tanstack/vue-select        Vue adapter
@tanstack/svelte-select     Svelte adapter

Optional text-filter utilities may live in core entry points if tree-shaking is reliable. Positioning, popovers, animation, and virtualization should stay integrations.

Core feature modules should contribute state, defaults, instance methods, option methods, and model stages. Likely built-in features are:

  • Selection
  • Query/filtering/ranking
  • Availability
  • Active-option navigation
  • Active-descendant and roving-focus strategies
  • Range selection
  • Grouping/expansion
  • Creation
  • Async/manual data

The public feature object should select and validate first-party features. We should not promise arbitrary third-party feature definitions in v1; that extension surface is difficult to evolve and is not required for tree-shaking.

Public API sketches

These examples illustrate the intended layers, not settled naming. The API should use predictable families: get*Model for derived data, get*Props for DOM bindings, verbs such as selectOption for actions, and on*Change callbacks with action metadata. That consistency should make the library legible to both people and coding agents.

Simple select

The common path should infer { value, label } options and support uncontrolled state.

const select = useSelect({
  options: [
    { value: 'red', label: 'Red' },
    { value: 'blue', label: 'Blue' },
  ],
  defaultValue: 'red',
})

return (
  <>
    <button {...select.getTriggerProps()}>
      {select.getSelectedOption()?.label}
    </button>

    {select.getIsOpen() && (
      <ul {...select.getListboxProps()}>
        {select.getVisibleOptionModel().flatOptions.map(option => (
          <li key={option.id} {...select.getOptionProps(option)}>
            {option.label}
          </li>
        ))}
      </ul>
    )}
  </>
)

Select owns state and interaction. The consumer owns markup, styling, positioning, and animation.

Arbitrary domain data and controlled state

Applications should not need to convert domain objects into library-owned wrappers.

const select = useSelect({
  options: users,
  getOptionId: user => user.id,
  getOptionValue: user => user.id,
  getOptionLabel: user => user.name,
  getOptionText: user => `${user.name} ${user.email}`,
  getOptionDisabled: user =>
    user.status === 'suspended' ? 'Account suspended' : false,

  value: selectedUserId,
  onValueChange: (value, meta) => setSelectedUserId(value),
})

select.getVisibleOptionModel().flatOptions.map(option => (
  <div key={option.id} {...select.getOptionProps(option)}>
    <UserResult user={option.original} />
  </div>
))

The option ID is engine identity, the value is application state, and original preserves the source object.

Feature-composed multi-select combobox

Advanced behavior should be explicit and tree-shakable.

const features = selectFeatures({
  combobox: comboboxFeature,
  selection: multipleSelectionFeature({
    allowDuplicates: true,
    rangeSelection: true,
  }),
  filtering: optionFilteringFeature,
  focus: activeDescendantFeature,
  filteredOptionModel: getFilteredOptionModel(),
  filterFns: { fuzzy },
})

const select = useSelect({
  features,
  options: people,
  getOptionId: person => person.id,
  getOptionValue: person => person.id,
  getOptionLabel: person => person.name,
  filterFn: 'fuzzy',

  state: { selection, query },
  onSelectionChange: (updater, meta) => setSelection(updater),
  onQueryChange: (updater, meta) => setQuery(updater),
})

return (
  <div {...select.getControlProps()}>
    {select.getSelections().map(selection => (
      <span
        key={selection.id}
        {...select.getSelectionProps(selection)}
      >
        {selection.getOption()?.label ?? String(selection.value)}
        <button {...select.getRemoveSelectionProps(selection)}>Remove</button>
      </span>
    ))}

    <input {...select.getInputProps()} />
  </div>
)

Duplicate occurrences have distinct selection IDs even when their option and value are equal. Omitting comboboxFeature produces a select-only control. Choosing rovingFocusFeature instead of activeDescendantFeature changes focus behavior where the widget pattern permits it. Invalid feature/model combinations should fail at compile time.

Manual async search with TanStack Query

Select coordinates query and interaction state without owning fetching.

const usersQuery = useQuery({
  queryKey: ['users', search],
  queryFn: ({ signal }) => fetchUsers({ search, signal }),
})

const select = useSelect({
  features: selectFeatures({
    combobox: comboboxFeature,
    filtering: manualFilteringFeature,
    focus: activeDescendantFeature,
  }),
  options: usersQuery.data ?? [],
  query: search,
  onQueryChange: setSearch,
  loading: usersQuery.isLoading,
  error: usersQuery.error,
})

Select owns input, active-option reconciliation, loading/error bindings, announcements, and unresolved selections. Query owns fetching, caching, cancellation, retry, and stale data.

Table-produced faceted filter

Table owns facet derivation; Select owns presentation and selection interaction.

const statusCounts = table
  .getColumn('status')
  .getFacetedUniqueValues()

const statusOptions = Array.from(statusCounts, ([status, count]) => ({
  status,
  count,
}))

const select = useSelect({
  options: statusOptions,
  getOptionId: option => option.status,
  getOptionValue: option => option.status,
  getOptionLabel: option => option.status,
  value: table.getColumn('status').getFilterValue(),
  onValueChange: value =>
    table.getColumn('status').setFilterValue(value),
})

select.getVisibleOptionModel().flatOptions.map(option => (
  <div key={option.id} {...select.getOptionProps(option)}>
    <span>{option.label}</span>
    <span>{option.original.count}</span>
  </div>
))

TanStack Virtual bridge

Select should request scrolling by stable option ID rather than owning measurement.

const visibleOptions = select.getVisibleOptionModel().flatOptions

const virtualizer = useVirtualizer({
  count: visibleOptions.length,
  getScrollElement: () => listRef.current,
})

select.onScrollRequest(request => {
  const index = visibleOptions.findIndex(
    option => option.id === request.optionId,
  )

  virtualizer.scrollToIndex(index, { align: request.align })
})

virtualizer.getVirtualItems().map(item => {
  const option = visibleOptions[item.index]

  return (
    <div key={option.id} {...select.getOptionProps(option)}>
      {option.label}
    </div>
  )
})

The core never requires every option to have a mounted element.

Inspectable model and actions

The instance should make its state and models directly inspectable for debugging, devtools, tests, and agent-authored integrations.

select.getState()
select.getOption(optionId)
select.getSelectedOptionModel()
select.getVisibleOptionModel()
select.getNavigableOptionModel()

select.open({ reason: 'programmatic' })
select.setQuery('lin', { reason: 'programmatic' })
select.setActiveOption(optionId, { reason: 'programmatic' })
select.selectOption(optionId, { reason: 'programmatic' })
select.clearSelection({ reason: 'programmatic' })

Required examples

Examples are part of the product specification:

  • Native-looking single select.
  • Searchable combobox.
  • Multiple select with removable tags and duplicate occurrences.
  • Creatable select with async validation.
  • Grouped and disabled options.
  • Table-produced faceted filter picker.
  • Remote search with loading, stale results, error, and pagination.
  • 100,000-option virtualized select using TanStack Virtual.
  • Controlled state and custom transition behavior.
  • Portaled popup in a dialog.
  • Form submission, validation, and reset.
  • Right-to-left layout and IME input.

Each behavior example should use unstyled semantic markup first. Styled examples can be separate.

Verification strategy

Core tests

  • Identity remains stable across reorder, filter, grouping, and async replacement.
  • Every model stage composes correctly and memoizes on unrelated state changes.
  • Single, multiple, duplicate, unresolved, disabled, and created selections behave losslessly.
  • Controlled and uncontrolled state produce the same transitions.
  • Action metadata identifies every cause.
  • Stale async results and failed creation cannot corrupt selection.

Interaction conformance

Use a shared behavior suite against every adapter:

  • Keyboard matrices for closed/open, empty/non-empty query, single/multiple, and LTR/RTL.
  • Pointer, touch, focus-within, outside dismissal, portals, and dialogs.
  • IME composition and mobile input.
  • Screen-reader-oriented ARIA assertions and automated accessibility checks.
  • Virtualized active-option and scroll coordination.
  • Form submit/reset/validation behavior.

Type tests

  • Arbitrary option and value types.
  • Single/multiple mode inference.
  • Nullable and unresolved values.
  • Custom accessors and updater callbacks.
  • Framework adapter prop types and ref types.

Delivery plan

Phase 0: reconcile the RFCs

  • Mark each item required for v1, designed now/shipped later, or out of scope.
  • Resolve the open decisions below before public API work.
  • Turn examples into executable acceptance fixtures.

Phase 1: core model and state

  • TypeScript package skeleton, static feature object, prerequisite validation, and TanStack Store-backed state slices.
  • Option identity/accessors and core option model.
  • Ordered selection entries, unresolved value resolution, and controlled state.
  • Query, availability, filtering/ranking, and navigable models.
  • Typed actions and transition metadata.

Phase 2: React interaction adapter

  • Combobox/listbox prop getters and focus/dismissal state machine.
  • Single, multiple, duplicate, disabled, and creatable behavior.
  • Portal and form support.
  • Shared conformance suite and unstyled examples.

React is the proving adapter because the existing behavior is React-based, not because the core may rely on React.

Phase 3: scale and data features

  • TanStack Virtual integration and benchmarks.
  • Manual/remote filtering, request identity, pagination, and unresolved-value hydration.
  • Grouping, expansion, and range selection.
  • TanStack Query integration guide.

Phase 4: additional adapters and release

  • Solid, Vue, and Svelte adapters running the shared behavior suite.
  • Documentation organized by concepts, APIs, and examples.
  • Bundle, performance, accessibility, browser, and type-compatibility gates.
  • Prerelease feedback before stabilizing v1.

Remaining open decisions

  1. Is v1's public abstraction an instance with option models, framework primitives/components, or both?
  2. Do we expose selection entries publicly, or keep them internal except when duplicate occurrences require them?
  3. What is the default value equality rule, and when is getOptionId required?
  4. Which state slices are core versus optional features?
  5. Which model stages beyond filtering and ranking are required for v1?
  6. Do nested options and arbitrary grouping share one model, or remain separate features?
  7. Should inline completion ship in core or as adapter-derived behavior?
  8. How much accessible markup flexibility can prop getters safely support?
  9. Do adapters expose only prop getters, optional unstyled primitives, or both?
  10. Is async request state in core, or is core limited to manual data plus documented integrations?
  11. Should create parsing return a value, an option, or an async result that can contain either?
  12. What are the exact close, query-reset, active-option, and Escape defaults for each mode?
  13. Which browser and assistive-technology matrix blocks v1?
  14. Do we expose custom third-party feature definitions in v1? This RFC recommends no; the first-party feature object remains public.

Definition of v1

V1 is ready when a consumer can build a production-grade, accessible, unstyled single or multiple combobox over arbitrary typed data; control any important state; search locally or remotely; create and resolve values; render through a portal; virtualize a large option model; integrate with forms; and rely on deterministic behavior covered by a cross-framework conformance suite.

Grouping, hierarchical models, and richer async helpers may be delivered incrementally, but their model boundaries must be present before the core API is stabilized. Otherwise the rewrite will reproduce the ceiling of a conventional combobox library instead of becoming TanStack Select.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions