How to Build Complex Filtering Interfaces with Astryx Selector Components

Use Astryx's Selector, MultiSelector, and ComplexSelector components to compose multi-dimensional filtering UIs without custom state management or styling logic.

Astryx provides a composable family of Selector components that handle dropdown rendering, keyboard navigation, accessibility, and theming out of the box. Whether you need a simple dropdown, a tag-style multi-select, or a sophisticated multi-facet filter bar, these components share a unified API built on StyleX styling and the useFocusableSelector hook. This guide explains how to assemble complex filtering interfaces using the source patterns found in facebook/astryx.


Core Selector Architecture

The Selector component in packages/core/src/Selector/Selector.tsx serves as the foundation. It accepts options via JSX children or the options prop and manages open/close state, focus trapping, and portal rendering.

Key capabilities implemented in the source:

  • Sections – Group options visually using the sections prop
  • Status badges – Built-in status prop for loading, error, and success states
  • Clearable selectionsclearable flag adds a native clear button
  • Ghost toolbar – Overlay actions via SelectorGhostToolbar.tsx without layout shifts

The SelectorOption component (packages/core/src/Selector/SelectorOption.tsx) renders individual items. It accepts label, icon, disabled, and arbitrary data props, making it suitable for avatars, badges, or custom markup.


Building Single-Select Filters

For straightforward filtering, compose Selector with SelectorOption children:

import { Selector, SelectorOption } from '@astryxdesign/core';

function StatusFilter({ value, onChange }) {
  return (
    <Selector
      value={value}
      onChange={onChange}
      placeholder="Select status"
      clearable
    >
      <SelectorOption value="open" icon="circle">Open</SelectorOption>
      <SelectorOption value="closed" icon="check-circle">Closed</SelectorOption>
      <SelectorOption value="in_progress" icon="clock">In Progress</SelectorOption>
    </Selector>
  );
}

This pattern matches the implementation in packages/cli/assets/templates/blocks/components/Selector/SelectorShowcase.tsx, which demonstrates sectioned options and custom rendering.


When users need to filter by multiple values in the same dimension, use MultiSelector (packages/core/src/MultiSelector/MultiSelector.tsx).

MultiSelector wraps Selector internals and adds:

  • Set-based state management for selected values
  • searchable prop for filtering long option lists
  • Tag-style display of selected items
  • Keyboard shortcuts for rapid selection (Space to toggle, Enter to confirm)
import { MultiSelector, SelectorOption } from '@astryxdesign/core';

function OwnerFilter({ value, onChange }) {
  return (
    <MultiSelector
      value={value}
      onChange={onChange}
      searchable
      clearable
      placeholder="Select owners"
      sections={[
        { title: 'Engineering', options: ['alice', 'bob'] },
        { title: 'Design', options: ['carol'] }
      ]}
    >
      <SelectorOption value="alice">Alice</SelectorOption>
      <SelectorOption value="bob">Bob</SelectorOption>
      <SelectorOption value="carol">Carol</SelectorOption>
    </MultiSelector>
  );
}

The showcase file packages/cli/assets/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx contains additional examples with async data loading and custom option rendering.


Multi-Facet Filtering with ComplexSelector

For interfaces requiring simultaneous filtering across multiple independent dimensions—status, owner, date range, labels—use ComplexSelector (packages/core/src/ComplexSelector/ComplexSelector.tsx).

ComplexSelector orchestrates state across child selectors and exposes a consolidated filter object via onChange. Each child selector registers itself using the name prop, and ComplexSelector aggregates values into a single state shape.

import { ComplexSelector } from '@astryxdesign/core';
import { SelectorOption } from '@astryxdesign/core';

function IssueFilters({ filters, onFiltersChange }) {
  return (
    <ComplexSelector
      value={filters}
      onChange={onFiltersChange}
    >
      {/* Single-select dimension */}
      <ComplexSelector.Selector name="status" placeholder="Status">
        <SelectorOption value="open">Open</SelectorOption>
        <SelectorOption value="closed">Closed</SelectorOption>
      </ComplexSelector.Selector>

      {/* Multi-select dimension with search */}
      <ComplexSelector.MultiSelector
        name="owner"
        placeholder="Owner"
        searchable
      >
        <SelectorOption value="alice">Alice</SelectorOption>
        <SelectorOption value="bob">Bob</SelectorOption>
        <SelectorOption value="carol">Carol</SelectorOption>
      </ComplexSelector.MultiSelector>

      {/* Another multi-select dimension */}
      <ComplexSelector.MultiSelector name="labels" placeholder="Labels">
        <SelectorOption value="bug">Bug</SelectorOption>
        <SelectorOption value="enhancement">Enhancement</SelectorOption>
        <SelectorOption value="question">Question</SelectorOption>
      </ComplexSelector.MultiSelector>
    </ComplexSelector>
  );
}

The resulting filters object has the shape:

{
  status: 'open' | 'closed' | null;
  owner: string[];
  labels: string[];
}

Keyboard Navigation and Accessibility

All Selector components inherit focus management from useFocusableSelector (packages/core/src/hooks/focusableSelector.ts). This hook provides:

  • Arrow key navigation between options
  • Home/End to jump to first/last option
  • Escape to close dropdown and return focus
  • Typeahead search for rapid option selection
  • Focus rings styled via StyleX

No additional code is required—these behaviors activate automatically when using Selector, MultiSelector, or ComplexSelector.


Customization Patterns

Use case Approach Source reference
Custom option content Pass renderOption prop to Selector Selector.tsx props interface
Async option loading Use status="loading" with onSearchChange MultiSelectorShowcase.tsx
Ghost toolbar actions Render SelectorGhostToolbar as child SelectorGhostToolbar.tsx
Programmatic control Imperative handle via ref with open(), close(), focus() focusableSelector.ts
Theming override Extend StyleX styles via style or className props StyleX documentation in repo

Key Source Files

Component File path Purpose
Selector packages/core/src/Selector/Selector.tsx Core dropdown with sections, status, clearable
SelectorOption packages/core/src/Selector/SelectorOption.tsx Single selectable item
MultiSelector packages/core/src/MultiSelector/MultiSelector.tsx Multi-select with tags and search
ComplexSelector packages/core/src/ComplexSelector/ComplexSelector.tsx Multi-facet filter orchestration
useFocusableSelector packages/core/src/hooks/focusableSelector.ts Keyboard navigation and focus management
Selector showcase packages/cli/assets/templates/blocks/components/Selector/SelectorShowcase.tsx Usage examples and visual tests
MultiSelector showcase packages/cli/assets/templates/blocks/components/MultiSelector/MultiSelectorShowcase.tsx Advanced patterns and async data

Source links:


Summary

  • Selector handles single-select dropdowns with sections, status indicators, and clearable state
  • SelectorOption renders individual items and supports custom content via composition
  • MultiSelector adds multi-select capabilities with search and tag-style display
  • ComplexSelector orchestrates multiple selectors into unified multi-facet filter UIs
  • useFocusableSelector provides keyboard navigation and accessibility across all variants
  • All components share StyleX-based theming for automatic dark/light mode support

Frequently Asked Questions

How do I add search to a MultiSelector?

Set the searchable prop on MultiSelector. The component filters visible options as the user types and exposes onSearchChange for async data fetching. See MultiSelectorShowcase.tsx for implementation patterns with debounced API calls.

Can I render custom content inside options?

Yes. Pass a renderOption function to Selector or MultiSelector, or wrap content directly in SelectorOption. The component preserves your markup while maintaining keyboard selection and focus management.

What state shape does ComplexSelector produce?

ComplexSelector aggregates child selector values into a flat object keyed by each child's name prop. Single-select children return string values or null; multi-select children return string arrays. The complete object is passed to onChange.

How do I programmatically open or close a Selector?

Use a ref with the imperative API exposed by useFocusableSelector: ref.current.open(), ref.current.close(), and ref.current.focus(). This bypasses React's declarative flow for integration with external triggers like keyboard shortcuts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →