Components Advanced
SearchableList
StableA search field paired with a filtered list, with result counts announced as you type.
Use SearchableList to let people find items in a list of tens to hundreds, such as organisation users or signature templates. In selectable mode, the search field drives a listbox for picking one item, like assigning a template owner. For choosing a value inside a form field, prefer Combobox.
Overview
8 results
- Priya Nairpriya.nair@binds.siSales
- Amara Okaforamara.okafor@binds.siLegal
- Lukas Brennerlukas.brenner@binds.siSales
- Hana Satohana.sato@binds.siFinance
- Tomás Ruiztomas.ruiz@binds.siOperations
- Owen Priceowen.price@binds.siSales
- Zainab Haddadzainab.haddad@binds.siLegal
- Mateja Kovačmateja.kovac@binds.siCustomer Success
import { Avatar, Badge, SearchableList } from '@bindshq/stance-react';
const users = [
{ id: 'u1', name: 'Priya Nair', email: 'priya.nair@binds.si', team: 'Sales' },
{ id: 'u2', name: 'Amara Okafor', email: 'amara.okafor@binds.si', team: 'Legal' },
{ id: 'u3', name: 'Lukas Brenner', email: 'lukas.brenner@binds.si', team: 'Sales' },
{ id: 'u4', name: 'Hana Sato', email: 'hana.sato@binds.si', team: 'Finance' },
{ id: 'u5', name: 'Tomás Ruiz', email: 'tomas.ruiz@binds.si', team: 'Operations' },
{ id: 'u6', name: 'Owen Price', email: 'owen.price@binds.si', team: 'Sales' },
{ id: 'u7', name: 'Zainab Haddad', email: 'zainab.haddad@binds.si', team: 'Legal' },
{ id: 'u8', name: 'Mateja Kovač', email: 'mateja.kovac@binds.si', team: 'Customer Success' },
];
export function Basic() {
return (
<div style={{ maxWidth: 420 }}>
<SearchableList
items={users}
getItemLabel={(u) => u.name}
getItemKey={(u) => u.id}
getItemKeywords={(u) => [u.email, u.team]}
searchLabel="Search organisation users"
placeholder="Search by name, email or team"
listLabel="Organisation users"
renderItem={(u) => (
<>
<Avatar name={u.name} size="sm" />
<span style={{ display: 'grid' }}>
<span>{u.name}</span>
<small style={{ color: 'var(--st-color-fg-muted)' }}>{u.email}</small>
</span>
<span style={{ marginLeft: 'auto' }}>
<Badge size="sm">{u.team}</Badge>
</span>
</>
)}
/>
</div>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
8 results
- Priya Nairpriya.nair@binds.siSales
- Amara Okaforamara.okafor@binds.siLegal
- Lukas Brennerlukas.brenner@binds.siSales
- Hana Satohana.sato@binds.siFinance
- Tomás Ruiztomas.ruiz@binds.siOperations
- Owen Priceowen.price@binds.siSales
- Zainab Haddadzainab.haddad@binds.siLegal
- Mateja Kovačmateja.kovac@binds.siCustomer Success
8 results
- Priya Nairpriya.nair@binds.siSales
- Amara Okaforamara.okafor@binds.siLegal
- Lukas Brennerlukas.brenner@binds.siSales
- Hana Satohana.sato@binds.siFinance
- Tomás Ruiztomas.ruiz@binds.siOperations
- Owen Priceowen.price@binds.siSales
- Zainab Haddadzainab.haddad@binds.siLegal
- Mateja Kovačmateja.kovac@binds.siCustomer Success
8 results
- Priya Nairpriya.nair@binds.siSales
- Amara Okaforamara.okafor@binds.siLegal
- Lukas Brennerlukas.brenner@binds.siSales
- Hana Satohana.sato@binds.siFinance
- Tomás Ruiztomas.ruiz@binds.siOperations
- Owen Priceowen.price@binds.siSales
- Zainab Haddadzainab.haddad@binds.siLegal
- Mateja Kovačmateja.kovac@binds.siCustomer Success
Import
import { SearchableList } from '@bindshq/stance-react';Platforms
SearchableList has native counterparts. They share Stance tokens and intent, and use each platform’s own controls and conventions. The live previews on this page are the web implementation.
import { SearchableList } from '@bindshq/stance-react';Examples
Pick one item
Arrow keys move through matches while focus stays in the search field.
8 results
- Priya Nair
- Amara Okafor
- Lukas Brenner
- Hana Sato
- Tomás Ruiz
- Owen Price
- Zainab Haddad
- Mateja Kovač
Owner: Amara Okafor
import * as React from 'react';
import { SearchableList } from '@bindshq/stance-react';
const users = [
{ id: 'u1', name: 'Priya Nair', email: 'priya.nair@binds.si', team: 'Sales' },
{ id: 'u2', name: 'Amara Okafor', email: 'amara.okafor@binds.si', team: 'Legal' },
{ id: 'u3', name: 'Lukas Brenner', email: 'lukas.brenner@binds.si', team: 'Sales' },
{ id: 'u4', name: 'Hana Sato', email: 'hana.sato@binds.si', team: 'Finance' },
{ id: 'u5', name: 'Tomás Ruiz', email: 'tomas.ruiz@binds.si', team: 'Operations' },
{ id: 'u6', name: 'Owen Price', email: 'owen.price@binds.si', team: 'Sales' },
{ id: 'u7', name: 'Zainab Haddad', email: 'zainab.haddad@binds.si', team: 'Legal' },
{ id: 'u8', name: 'Mateja Kovač', email: 'mateja.kovac@binds.si', team: 'Customer Success' },
];
export function Selectable() {
const [owner, setOwner] = React.useState<React.Key | null>('u2');
return (
<div style={{ maxWidth: 380, display: 'grid', gap: 8 }}>
<SearchableList
items={users}
getItemLabel={(u) => u.name}
getItemKey={(u) => u.id}
getItemKeywords={(u) => [u.team]}
selectable
selectedKey={owner}
onSelectionChange={setOwner}
searchLabel="Find a template owner"
listLabel="People who can own templates"
maxHeight={220}
/>
<p style={{ margin: 0 }}>Owner: {users.find((u) => u.id === owner)?.name ?? 'none'}</p>
</div>
);
}Fuzzy ranking
matchMode="fuzzy" ranks prefix and word matches first and tolerates gaps.
3 of 6 results
- Standard signature
- Sales team signature
- Recruiting signature
import { SearchableList } from '@bindshq/stance-react';
export function Fuzzy() {
const templates = [
'Standard signature',
'Sales team signature',
'Legal disclaimer footer',
'Holiday out-of-office',
'Event banner, Q4 summit',
'Recruiting signature',
];
return (
<div style={{ maxWidth: 380 }}>
<SearchableList
items={templates}
getItemLabel={(t) => t}
matchMode="fuzzy"
defaultQuery="sgn"
searchLabel="Search templates"
listLabel="Signature templates"
emptyState={(q) => <p>No templates match “{q}”. Try a shorter search.</p>}
/>
</div>
);
}API reference
| Prop | Type | Default | Description |
|---|---|---|---|
items* required | readonly T[] | None | Items to filter. |
getItemLabel* required | (item: T) => string | None | Text used for matching and as the option name. |
listLabel* required | string | None | Accessible name of the list. |
searchLabel | string | 'Search' | Accessible name of the search field. |
getItemKey | (item: T) => Key | label | Stable key; also the selection key. |
getItemKeywords | (item: T) => string[] | None | Extra searchable text, such as email or team. |
renderItem | (item: T, state) => ReactNode | None | Custom item content. state has active, selected and query. |
matchMode | 'contains' | 'fuzzy' | 'contains' | Contains keeps original order; fuzzy ranks matches. |
filter | (item: T, query: string) => boolean | None | Custom matching; overrides matchMode. |
query / defaultQuery / onQueryChange | string / string / (q) => void | None | Controlled or uncontrolled search text. |
selectable | boolean | false | Turns the list into a single-select listbox. |
selectedKey / defaultSelectedKey | Key | null | None | Selected item, controlled or uncontrolled. |
onSelectionChange | (key, item) => void | None | Called when an item is selected. |
isItemDisabled | (item: T) => boolean | None | Disabled items are skipped and cannot be selected. |
emptyState | ReactNode | (query) => ReactNode | None | Shown when nothing matches. |
formatCount | (count, total) => string | None | Text for the results status. |
maxHeight | number | string | 320 | Max list height before it scrolls. |
size | 'sm' | 'md' | 'md' | Field and row density. |
Accessibility
- The result count is a status live region, so screen reader users hear “3 of 48 results” as they type.
- In selectable mode the search field is a combobox that controls the listbox; aria-activedescendant tracks the active option while focus stays in the field.
- Escape clears the search, as in SearchInput.
- Disabled items stay visible and are announced as unavailable.
Keyboard
| Keys | Action |
|---|---|
| ↓ ↑ | Selectable: move the active option. |
| Ctrl Home | Selectable: first option. |
| Ctrl End | Selectable: last option. |
| Enter | Selectable: select the active option. |
| Escape | Clear the search. |
Usage guidelines
Do
- Include secondary text, such as email or team, in getItemKeywords.
- Write an empty state that suggests what to try next.
Don’t
- Use for thousands of items without virtualisation; combine with VirtualizedList.
- Use as a form field picker; use Combobox.