Skip to content

SearchFilterInput

Structured search control where each tag represents a field/operator/value filter.

() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([]);
const {config} = useSearchFilterInputConfig(basicFields);
return (
<SearchFilterInput
config={config}
filters={filters}
onChange={next => setFilters(next)}
/>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([]);
const {config} = useSearchFilterInputConfig([
{
enumValues: DEPARTMENTS,
key: 'department',
label: 'Department',
type: 'enum',
},
] as const);
return (
<SearchFilterInput
config={config}
filters={filters}
onChange={next => setFilters(next)}
/>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'John'},
},
{
field: 'status',
operator: 'is',
value: {type: 'enum', value: 'active'},
},
]);
const {config} = useSearchFilterInputConfig(basicFields);
return (
<SearchFilterInput
config={config}
filters={filters}
onChange={next => setFilters(next)}
/>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([
{
field: 'name',
operator: 'is_not_set',
value: {type: 'empty'},
},
]);
return (
<SearchFilterInput
config={emptyOperatorConfig}
filters={filters}
onChange={next => setFilters(next)}
/>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([]);
const {config} = useSearchFilterInputConfig([
{key: 'name', label: 'Name', type: 'string'},
{key: 'tags', label: 'Tags', type: 'string_list'},
{key: 'count', label: 'Count', type: 'number'},
{key: 'active', label: 'Active', type: 'boolean'},
{key: 'created', label: 'Created', type: 'date'},
{key: 'status', label: 'Status', type: 'enum', enumValues: STATUSES},
{
key: 'priority',
label: 'Priority',
type: 'enum_list',
enumValues: PRIORITIES,
},
] as const);
return (
<SearchFilterInput
config={config}
filters={filters}
onChange={next => setFilters(next)}
/>
);
}
() => {
const [smFilters, setSmFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'Alice'},
},
]);
const [mdFilters, setMdFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'Alice'},
},
]);
const [lgFilters, setLgFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'Alice'},
},
]);
const {config} = useSearchFilterInputConfig(basicFields);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
<SearchFilterInput
config={config}
filters={smFilters}
label="Small"
onChange={next => setSmFilters(next)}
placeholder="Small..."
size="sm"
/>
<SearchFilterInput
config={config}
filters={mdFilters}
label="Medium"
onChange={next => setMdFilters(next)}
placeholder="Medium..."
size="md"
/>
<SearchFilterInput
config={config}
filters={lgFilters}
label="Large"
onChange={next => setLgFilters(next)}
placeholder="Large..."
size="lg"
/>
</div>
);
}
Name containsJohn
() => {
const {config} = useSearchFilterInputConfig(basicFields);
return (
<SearchFilterInput
config={config}
filters={[
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'John'},
},
]}
isDisabled
onChange={() => {}}
/>
);
}
Name containsJohnStatus isActive
() => {
const {config} = useSearchFilterInputConfig(basicFields);
return (
<SearchFilterInput
config={config}
filters={[
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'John'},
},
{
field: 'status',
operator: 'is',
value: {type: 'enum', value: 'active'},
},
]}
isReadOnly
onChange={() => {}}
/>
);
}
42 results
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([]);
const {config} = useSearchFilterInputConfig(basicFields);
return (
<SearchFilterInput
config={config}
filters={filters}
onChange={next => setFilters(next)}
resultCount={42}
/>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([]);
const {config} = useSearchFilterInputConfig(basicFields);
return (
<SearchFilterInput
config={config}
filters={filters}
isLabelHidden={false}
label="Filter users"
onChange={next => setFilters(next)}
/>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([]);
const {config} = useSearchFilterInputConfig(basicFields);
return (
<SearchFilterInput
config={config}
filters={filters}
onChange={next => setFilters(next)}
status={{type: 'error', message: 'At least one filter is required'}}
/>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'Alice'},
},
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'Bob'},
},
{
field: 'status',
operator: 'is',
value: {type: 'enum', value: 'active'},
},
{
field: 'status',
operator: 'is',
value: {type: 'enum', value: 'pending'},
},
{
field: 'age',
operator: 'equals',
value: {type: 'integer', value: 30},
},
{
field: 'name',
operator: 'contains',
value: {type: 'string', value: 'Charlie'},
},
]);
const {config} = useSearchFilterInputConfig(basicFields);
return (
<div style={{maxWidth: 600}}>
<SearchFilterInput
config={config}
filters={filters}
onChange={next => setFilters(next)}
tagOverflowBehavior="unfocusedInline"
/>
</div>
);
}
() => {
const [filters, setFilters] = useState<
ReadonlyArray<SearchFilterInputFilter>
>([
{
field: 'status',
operator: 'is',
value: {type: 'enum', value: 'active'},
},
]);
const {config} = useSearchFilterInputConfig(basicFields);
// Override the tag and editor for the `enum` value type only; all other
// field types keep the default rendering. Click the pill to open the custom
// editor.
const components: SearchFilterInputComponents = {
enum: {Editor: StatusPickerEditor, Tag: StatusBadgeTag},
};
return (
<SearchFilterInput
components={components}
config={config}
filters={filters}
onChange={next => setFilters(next)}
/>
);
}
() => {
const {config} = useSearchFilterInputConfig([
{key: 'created', label: 'Created', type: 'date'},
] as const);
// A single fixed instant (2026-01-15 12:00 UTC). Each input below renders
// the same filter with a different timezoneID, so the tag's formatted
// date/time differs per zone.
const unixSeconds = Math.floor(
Temporal.Instant.from('2026-01-15T12:00:00Z').epochMilliseconds / 1000,
);
const filters: ReadonlyArray<SearchFilterInputFilter> = [
{
field: 'created',
operator: 'after',
value: {type: 'date_absolute', unixSeconds},
},
];
const timezones = [
'America/Los_Angeles',
'America/New_York',
'Europe/London',
'Asia/Tokyo',
];
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
{timezones.map(timezone => (
<SearchFilterInput
config={config}
filters={filters}
isLabelHidden={false}
key={timezone}
label={timezone}
onChange={() => {}}
timezoneID={timezone}
/>
))}
</div>
);
}

Structured search control where each tag represents a field/operator/value filter.

PropTypeDefaultDescription
classNamestringAdditional CSS class names applied to the input wrapper.
componentsSearchFilterInputComponentsPer-value-type component overrides.
config*SearchFilterInputConfigSearchFilterInput field/operator configuration.
data-testidstringTest ID applied to the input wrapper.
endContentReactNodeContent displayed at the end of the input row.
filters*ReadonlyArray<SearchFilterInputFilter>Current filters.
handleRefRef<SearchFilterInputHandle>Imperative focus/blur handle.
hasAutoFocusbooleanfalseWhether to focus the input on mount.
hasClearbooleantrueWhether to show a clear button.
isDisabledbooleanfalseWhether the input is disabled.
isLabelHiddenbooleantrueWhether to visually hide the label.
isReadOnlybooleanfalseWhether filters are read-only.
labelstring'Search'Accessible label.
maxOperatorMenuItemsnumberMaximum number of items shown in the operator dropdown menu. When set, only the first N operators are displayed.
maxTagLengthnumber40Maximum displayed tag value length.
onBlur(event: FocusEvent<HTMLDivElement>) => voidCalled when focus leaves the control.
onChange*( filters: ReadonlyArray<SearchFilterInputFilter>, changeType: SearchFilterInputChangeType, index: number, ) => voidCalled when filters change.
onFocus(event: FocusEvent<HTMLDivElement>) => voidCalled when focus enters the control.
placeholderstring'Search...'Placeholder text.
popoverSaveButtonLabelstring'Apply'Save button label in the edit popover.
refRef<HTMLDivElement>Ref forwarded to the root element.
resultCountnumber | stringResult count displayed at end of row.
size"sm" | "md" | "lg"'md'Visual size.
startIconIconComponentIcon shown before the input.
statusInputStatusValidation status displayed below the input.
styleCSSPropertiesInline styles applied to the input wrapper.
tagOverflowBehavior"none" | "unfocusedInline" | "unfocusedLayer"'none'Controls how tags overflow when the container is too narrow. - 'none': Tags wrap to multiple lines (default). - 'unfocusedInline': Single line with "+ N more" when unfocused; expands inline on focus. - 'unfocusedLayer': Single line with "+ N more" when unfocused; expands as overlay on focus.
timezoneIDstringTimezone ID for date formatting.
PropTypeDefaultDescription
config*InternalSearchFilterInputConfigInternal config lookup helpers.
filter*PartialFilterPartial filter being created or edited.
isReadOnlybooleanfalseWhether controls are read-only.
maxOperatorMenuItemsnumberMaximum number of items shown in the operator dropdown menu.
mode*'create' | 'edit'Editor mode.
onCancel*() => voidCalled when editing is cancelled.
onSave*(filter: SearchFilterInputFilter | null) => voidCalled with a completed filter, or null to delete.
saveButtonLabelstring'Apply'Save button label.

Default tag renderer for a SearchFilterInput filter.

PropTypeDefaultDescription
config*SearchFilterInputConfig
field*SearchFilterInputField
filter*SearchFilterInputFilter
isDisabledboolean
maxLength*number
onClick() => void
onRemove() => void
operator*SearchFilterInputOperator