Platform-agnostic utility components that serve as building blocks for verticals. These components provide common functionality like sorting and filtering without being tied to any specific business domain.
npm install @wix/headless-componentsPlatform-agnostic utility components that serve as building blocks for verticals. These components provide common functionality like sorting and filtering without being tied to any specific business domain.
Provides flexible sort controls with both declarative and programmatic APIs. Uses @radix-ui/react-select for accessible dropdown functionality while maintaining a simple interface.
AsChild Pattern: All components support the asChild prop which follows the Radix UI pattern. When asChild is true, the component renders its child element instead of its default element, forwarding all props and refs.
#### Basic Usage
``tsx
import { Sort } from '@wix/headless-components/react';
// Declarative API with Radix Select (default styling)
function SortDropdown({ sort, onChange }) {
const sortOptions = [
{ fieldName: 'price', label: 'Price: Low to High', order: 'ASC' },
{ fieldName: 'price', label: 'Price: High to Low', order: 'DESC' },
{ fieldName: 'name', label: 'Name: A to Z', order: 'ASC' },
{ fieldName: 'name', label: 'Name: Z to A', order: 'DESC' },
];
return (
onChange={onChange}
sortOptions={sortOptions}
as="select"
placeholder="Choose sort order"
className="w-full"
/>
);
}
// Custom Radix Select trigger with asChild
function CustomTriggerSort({ sort, onChange, sortOptions }) {
return (
onChange={onChange}
sortOptions={sortOptions}
as="select"
asChild
>
);
}
// Advanced Radix Select customization
function AdvancedRadixSort({ sort, onChange, sortOptions }) {
return (
{/ Automatically renders all sort options /}
{/ Or manually create items /}
);
}
// Programmatic API with custom button controls (no Radix Select)
function ButtonSort({ sort, onChange }) {
return (
);
}
// Enhanced usage with Radix UI components Active filters:
function EnhancedFilterExample({ filter, onChange, onFilterChange, filterOptions }) {
return (
onChange={onChange}
onFilterChange={onFilterChange}
filterOptions={filterOptions}
className="p-6 border rounded-lg"
>
className="text-blue-600 underline hover:text-blue-800"
/>
{/ Enhanced SingleFilter with Radix ToggleGroup (default) /} {/ Custom SingleFilter with asChild /} {/ Enhanced MultiFilter with Radix ToggleGroup (default) /} {/ Enhanced RangeFilter with Radix Slider (default) - smooth interaction /} {/ Custom RangeFilter with asChild /}
{({ value, onChange, validValues, valueFormatter }) => (
{validValues?.map(option => (
key={option}
onClick={() => onChange(String(option))}
className={px-3 py-2 rounded-lg transition-all ${
value === String(option)
? 'bg-blue-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
}}
>
{valueFormatter ? valueFormatter(option) : option}
))}
)}
{({ value, onChange, validValues, valueFormatter }) => (
type="range"
min={validValues?.[0] || 0}
max={validValues?.[1] || 100}
value={value[0] || 0}
onChange={(e) => onChange([Number(e.target.value), value[1] || 100])}
className="w-full"
/>
{valueFormatter ? valueFormatter(value[0] || 0) : (value[0] || 0)}
{valueFormatter ? valueFormatter(value[1] || 100) : (value[1] || 100)}
)}
);
}
// AsChild pattern examples (following Radix UI/MediaGallery pattern)
function AsChildExamples({ sort, onChange, sortOptions }) {
return (
{/ Option with asChild - renders as custom button /}
{/ Select trigger with asChild /}
// Mixed approach - Radix Select for some options, buttons for others
function MixedSort({ sort, onChange, sortOptions }) {
return (
{/ Buttons for order selection /}
#### API Reference
Sort.Root
-
value: Sort - Current sort state
- onChange: (value: Sort) => void - Sort change handler
- sortOptions?: SortOption[] - Predefined options for declarative API
- as?: 'select' | 'list' - Render mode ('select' uses Radix UI, 'list' provides render props)
- placeholder?: string - Placeholder text for select mode
- asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionSort.Option
-
fieldName?: string - Field to sort by
- order?: 'ASC' | 'DESC' - Sort order
- label?: string - Display label
- asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionSort.RadixSelect.* - Radix UI Select Primitives
When using advanced customization, you can access all Radix Select primitives:
-
Sort.RadixSelect.Root - Root select container (automatically handles Sort context)
- Sort.RadixSelect.Trigger - Select trigger button
- Sort.RadixSelect.Value - Selected value display
- Sort.RadixSelect.Icon - Trigger icon
- Sort.RadixSelect.Content - Dropdown content (includes Portal)
- Sort.RadixSelect.Viewport - Scrollable viewport
- Sort.RadixSelect.Options - Auto-generates options from sortOptions
- Sort.RadixSelect.Item - Individual option item
- Sort.RadixSelect.ItemText - Option text contentAll Radix Select components support their standard props and can be styled as needed.
$3
Provides flexible filter controls supporting single selection, multi-selection, and range filters. Uses
@radix-ui/react-toggle-group for selections and @radix-ui/react-slider for ranges while maintaining a simple, platform-agnostic interface.AsChild Pattern: All components support the
asChild prop which follows the Radix UI pattern. When asChild is true, the component renders its child element instead of its default element, forwarding all props and refs.Enhanced Default Components: The filter components now include enhanced default implementations:
- SingleFilter: Uses Radix ToggleGroup (single mode) by default
- MultiFilter: Uses Radix ToggleGroup (multiple mode) by default
- RangeFilter: Uses Radix Slider by default with value formatting
#### Basic Usage
`tsx
import { Filter } from '@wix/headless-components/react';function ProductFilters({ filter, onChange, onFilterChange }) {
const filterOptions = [
{
key: 'category',
label: 'Category',
type: 'single',
displayType: 'text',
validValues: ['electronics', 'clothing', 'books'],
},
{
key: 'brand',
label: 'Brand',
type: 'multi',
displayType: 'text',
validValues: ['apple', 'samsung', 'nike', 'adidas'],
},
{
key: 'price',
label: 'Price Range',
type: 'range',
displayType: 'range',
validValues: [0, 1000],
valueFormatter: (value) =>
$${value},
},
]; return (
value={filter}
onChange={onChange}
onFilterChange={onFilterChange}
filterOptions={filterOptions}
className="space-y-4"
>
Active filters:
label="Clear All"
className="text-blue-600 underline hover:text-blue-800"
/>
);
}
// Custom filter rendering with asChild
function CustomFilters({ filter, onChange, onFilterChange, filterOptions }) {
return (
value={filter}
onChange={onChange}
onFilterChange={onFilterChange}
filterOptions={filterOptions}
>
{({ label }) => {label}
}
{({ value, onChange, validValues, valueFormatter }) => (
{validValues?.map(option => (
key={option}
onClick={() => onChange(String(option))}
className={px-3 py-1 rounded ${
value === String(option) ? 'bg-blue-500 text-white' : 'bg-gray-200'
}}
>
{valueFormatter ? valueFormatter(option) : option}
))}
)}
{({ values, onChange, validValues, valueFormatter }) => (
{validValues?.map(option => (
))}
)}
{({ value, onChange, validValues, valueFormatter }) => (
type="range"
min={validValues?.[0] || 0}
max={validValues?.[1] || 100}
value={value[0] || validValues?.[0] || 0}
onChange={(e) => onChange([Number(e.target.value), value[1] || validValues?.[1] || 100])}
className="w-full"
/>
{valueFormatter ? valueFormatter(value[0] || validValues?.[0] || 0) : (value[0] || validValues?.[0] || 0)}
{valueFormatter ? valueFormatter(value[1] || validValues?.[1] || 100) : (value[1] || validValues?.[1] || 100)}
)}
);
}
`#### API Reference
Filter.Root
-
value: Filter - Current filter state
- onChange: (value: Filter) => void - Filter change handler
- onFilterChange: ({ value, key }) => Filter - Single field update handler
- filterOptions: FilterOption[] - Available filter configurations
- asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionFilter.Filtered
-
children: React.ReactNode - Content to show when filters are activeFilter.Action.Clear
-
label: string - Button label
- asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionFilter.FilterOptions
-
children: React.ReactNode - Filter option componentsFilter.FilterOptionRepeater
-
children: React.ReactNode - Template for each filter optionFilter.FilterOption.Label
-
asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionFilter.FilterOption.SingleFilter
-
asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionFilter.FilterOption.MultiFilter
-
asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionFilter.FilterOption.RangeFilter
-
asChild?: boolean - Enable render prop pattern
- children?: React.ReactNode | RenderFunction - Children or render functionArchitecture
These components follow the headless UI pattern:
1. Unstyled: No default styling, only functional behavior
2. Composable: Support for the
asChild pattern for flexible DOM structure
3. Accessible: Built-in keyboard navigation and ARIA attributes
4. Flexible: Render props pattern for maximum customizationThe components are designed to be platform-agnostic and can be used across different verticals within the Wix ecosystem.
Integration with Stores
While these are platform components, they integrate seamlessly with the stores package:
`tsx
import { Sort, Filter } from '@wix/headless-components/react';
import { ProductListSort } from '@wix/stores/react';// Bridge platform components with store-specific logic
function ProductSortControls() {
return (
{({ selectedSortOption, updateSortOption, sortOptions }) => {
// Convert store sort options to platform format
const platformSortOptions = sortOptions.map(option => ({
fieldName: option.includes('price') ? 'price' : 'name',
order: option.includes('desc') ? 'DESC' : 'ASC',
label: formatSortLabel(option),
}));
const currentSort = {
fieldName: selectedSortOption.includes('price') ? 'price' : 'name',
order: selectedSortOption.includes('desc') ? 'DESC' : 'ASC',
};
return (
value={currentSort}
onChange={(sort) => {
const storeFormat =
${sort.fieldName}_${sort.order};
updateSortOption(storeFormat);
}}
sortOptions={platformSortOptions}
as="select"
/>
);
}}
);
}
``