Filter chip A filter in a toolbar above a list or table, such as Status or Channel: a dashed chip with a plus that opens a Popup holding a Listbox or a small panel, and shows what's chosen. For picking a value in a form, use a Select .
Installation bun add @harmo-ui/css @harmo-ui/react npm install @harmo-ui/css @harmo-ui/react pnpm add @harmo-ui/css @harmo-ui/react yarn add @harmo-ui/css @harmo-ui/react bunx harmo-ui add filter-chip npx harmo-ui add filter-chip pnpm dlx harmo-ui add filter-chip yarn dlx harmo-ui add filter-chip Usage import { FilterChip , Option } from "@harmo-ui/react" ;
< FilterChip
label = "Priority"
multiple
defaultValue = { [ "high" , "urgent" ] }
clearLabel = "Clear filters"
formatCount = { ( count ) => ` ${ count } selected` }
>
< Option value = "low" >Low</ Option >
< Option value = "medium" >Medium</ Option >
< Option value = "high" >High</ Option >
< Option value = "urgent" >Urgent</ Option >
</ FilterChip >
View full source
Examples Nothing chosen A dashed, fully rounded chip with a plus and the filter's name. It opens a Popup through popovertarget, like any trigger, and carries aria-expanded. Choose options to see them appear on the chip.
< FilterChip
label = "Status"
multiple
clearLabel = "Clear filters"
formatCount = { ( count ) => ` ${ count } selected` }
>
< Option value = "delivered" >Delivered</ Option >
< Option value = "read" >Read</ Option >
< Option value = "failed" >Failed</ Option >
< Option value = "pending" >Pending</ Option >
</ FilterChip >
View full source
< button
class = "hfilter-chip"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "status-filter"
style = "anchor-name: --status-filter"
>
< svg class = "hicon" width = "18" height = "18" viewBox = "0 0 24 25" fill = "none" aria-hidden = "true" focusable = "false" >
< path d = "M12 23.625C6.07 23.625 1.25 18.805 1.25 12.875C1.25 6.945 6.07 2.125 12 2.125C17.93 2.125 22.75 6.945 22.75 12.875C22.75 18.805 17.93 23.625 12 23.625ZM12 3.625C6.9 3.625 2.75 7.775 2.75 12.875C2.75 17.975 6.9 22.125 12 22.125C17.1 22.125 21.25 17.975 21.25 12.875C21.25 7.775 17.1 3.625 12 3.625Z" fill = "currentColor" />
< path d = "M16 13.625H8C7.59 13.625 7.25 13.285 7.25 12.875C7.25 12.465 7.59 12.125 8 12.125H16C16.41 12.125 16.75 12.465 16.75 12.875C16.75 13.285 16.41 13.625 16 13.625Z" fill = "currentColor" />
< path d = "M12 17.625C11.59 17.625 11.25 17.285 11.25 16.875V8.875C11.25 8.465 11.59 8.125 12 8.125C12.41 8.125 12.75 8.465 12.75 8.875V16.875C12.75 17.285 12.41 17.625 12 17.625Z" fill = "currentColor" />
</ svg >
< span class = "hfilter-chip-label" >Status</ span >
</ button >
< div class = "hpopup" id = "status-filter" popover style = "position-anchor: --status-filter" >
< ul class = "hlistbox" role = "listbox" aria-label = "Status" aria-multiselectable = "true" tabindex = "-1" >
< li class = "hoption" id = "status-delivered" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Delivered</ span >
</ li >
< li class = "hoption" id = "status-read" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Read</ span >
</ li >
< li class = "hoption" id = "status-failed" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Failed</ span >
</ li >
< li class = "hoption" id = "status-pending" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Pending</ span >
</ li >
</ ul >
</ div >
View full source
One value Once something is chosen, .hfilter-chip-values follows the label after a divider, with each value in a .hfilter-chip-value. The values become part of the chip's name: "Channel WhatsApp". A single-select chip closes when an option is chosen.
< FilterChip label = "Channel" defaultValue = "whatsapp" >
< Option value = "whatsapp" >WhatsApp</ Option >
< Option value = "telegram" >Telegram</ Option >
< Option value = "email" >Email</ Option >
</ FilterChip > < button
class = "hfilter-chip"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "channel-filter"
style = "anchor-name: --channel-filter"
>
< svg class = "hicon" width = "18" height = "18" viewBox = "0 0 24 25" fill = "none" aria-hidden = "true" focusable = "false" >
< path d = "M12 23.625C6.07 23.625 1.25 18.805 1.25 12.875C1.25 6.945 6.07 2.125 12 2.125C17.93 2.125 22.75 6.945 22.75 12.875C22.75 18.805 17.93 23.625 12 23.625ZM12 3.625C6.9 3.625 2.75 7.775 2.75 12.875C2.75 17.975 6.9 22.125 12 22.125C17.1 22.125 21.25 17.975 21.25 12.875C21.25 7.775 17.1 3.625 12 3.625Z" fill = "currentColor" />
< path d = "M16 13.625H8C7.59 13.625 7.25 13.285 7.25 12.875C7.25 12.465 7.59 12.125 8 12.125H16C16.41 12.125 16.75 12.465 16.75 12.875C16.75 13.285 16.41 13.625 16 13.625Z" fill = "currentColor" />
< path d = "M12 17.625C11.59 17.625 11.25 17.285 11.25 16.875V8.875C11.25 8.465 11.59 8.125 12 8.125C12.41 8.125 12.75 8.465 12.75 8.875V16.875C12.75 17.285 12.41 17.625 12 17.625Z" fill = "currentColor" />
</ svg >
< span class = "hfilter-chip-label" >Channel</ span >
< span class = "hfilter-chip-values" >
< span class = "hfilter-chip-value" >WhatsApp</ span >
</ span >
</ button >
< div class = "hpopup" id = "channel-filter" popover style = "position-anchor: --channel-filter" >
< ul class = "hlistbox" role = "listbox" aria-label = "Channel" tabindex = "-1" >
< li class = "hoption" id = "channel-whatsapp" role = "option" aria-selected = "true" >
< span class = "hoption-label" >WhatsApp</ span >
< svg class = "hoption-check" viewBox = "5.845 5.87 14 14" aria-hidden = "true" focusable = "false" >
< path
d = "M11.4261 16.455C11.2261 16.455 11.0361 16.375 10.8961 16.235L8.06613 13.405C7.77613 13.115 7.77613 12.635 8.06613 12.345C8.35613 12.055 8.83613 12.055 9.12613 12.345L11.4261 14.645L16.5661 9.505C16.8561 9.215 17.3361 9.215 17.6261 9.505C17.9161 9.795 17.9161 10.275 17.6261 10.565L11.9561 16.235C11.8161 16.375 11.6261 16.455 11.4261 16.455Z"
fill = "currentColor"
/>
</ svg >
</ li >
< li class = "hoption" id = "channel-telegram" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Telegram</ span >
</ li >
< li class = "hoption" id = "channel-email" role = "option" aria-selected = "false" >
< span class = "hoption-label" >Email</ span >
</ li >
</ ul >
</ div >
View full source
Two values A multi-select chip shows each chosen value, up to two. While anything is chosen, its list ends with Clear filters in a .hlistbox-footer, so values don't have to be removed one by one. Render the footer only then.
< FilterChip
label = "Priority"
multiple
defaultValue = { [ "high" , "urgent" ] }
clearLabel = "Clear filters"
formatCount = { ( count ) => ` ${ count } selected` }
>
< Option value = "low" >Low</ Option >
< Option value = "medium" >Medium</ Option >
< Option value = "high" >High</ Option >
< Option value = "urgent" >Urgent</ Option >
</ FilterChip >
View full source
< button
class = "hfilter-chip"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "priority-filter"
style = "anchor-name: --priority-filter"
>
< svg class = "hicon" width = "18" height = "18" viewBox = "0 0 24 25" fill = "none" aria-hidden = "true" focusable = "false" >
< path d = "M12 23.625C6.07 23.625 1.25 18.805 1.25 12.875C1.25 6.945 6.07 2.125 12 2.125C17.93 2.125 22.75 6.945 22.75 12.875C22.75 18.805 17.93 23.625 12 23.625ZM12 3.625C6.9 3.625 2.75 7.775 2.75 12.875C2.75 17.975 6.9 22.125 12 22.125C17.1 22.125 21.25 17.975 21.25 12.875C21.25 7.775 17.1 3.625 12 3.625Z" fill = "currentColor" />
< path d = "M16 13.625H8C7.59 13.625 7.25 13.285 7.25 12.875C7.25 12.465 7.59 12.125 8 12.125H16C16.41 12.125 16.75 12.465 16.75 12.875C16.75 13.285 16.41 13.625 16 13.625Z" fill = "currentColor" />
< path d = "M12 17.625C11.59 17.625 11.25 17.285 11.25 16.875V8.875C11.25 8.465 11.59 8.125 12 8.125C12.41 8.125 12.75 8.465 12.75 8.875V16.875C12.75 17.285 12.41 17.625 12 17.625Z" fill = "currentColor" />
</ svg >
< span class = "hfilter-chip-label" >Priority</ span >
< span class = "hfilter-chip-values" >
< span class = "hfilter-chip-value" >High</ span >
< span class = "hfilter-chip-value" >Urgent</ span >
</ span >
</ button >
< div class = "hpopup" id = "priority-filter" popover style = "position-anchor: --priority-filter" >
< ul class = "hlistbox" role = "listbox" aria-label = "Priority" aria-multiselectable = "true" tabindex = "-1" >
< li class = "hoption" id = "priority-low" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Low</ span >
</ li >
< li class = "hoption" id = "priority-medium" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Medium</ span >
</ li >
< li class = "hoption" id = "priority-high" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >High</ span >
</ li >
< li class = "hoption" id = "priority-urgent" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Urgent</ span >
</ li >
</ ul >
< div class = "hlistbox-footer" >
< button class = "hbutton" data-variant = "tertiary" type = "button" >
< span class = "hbutton-label" >Clear filters</ span >
</ button >
</ div >
</ div >
View full source
More than two Beyond two values the chip shows a count instead. The app supplies the wording.
< FilterChip
label = "Tags"
multiple
defaultValue = { [ "refund" , "invoice" , "bug" ] }
clearLabel = "Clear filters"
formatCount = { ( count ) => ` ${ count } selected` }
>
< Option value = "refund" >Refund</ Option >
< Option value = "invoice" >Invoice</ Option >
< Option value = "bug" >Bug report</ Option >
< Option value = "feature" >Feature request</ Option >
</ FilterChip >
View full source
< button
class = "hfilter-chip"
type = "button"
aria-haspopup = "listbox"
aria-expanded = "false"
popovertarget = "tags-filter"
style = "anchor-name: --tags-filter"
>
< svg class = "hicon" width = "18" height = "18" viewBox = "0 0 24 25" fill = "none" aria-hidden = "true" focusable = "false" >
< path d = "M12 23.625C6.07 23.625 1.25 18.805 1.25 12.875C1.25 6.945 6.07 2.125 12 2.125C17.93 2.125 22.75 6.945 22.75 12.875C22.75 18.805 17.93 23.625 12 23.625ZM12 3.625C6.9 3.625 2.75 7.775 2.75 12.875C2.75 17.975 6.9 22.125 12 22.125C17.1 22.125 21.25 17.975 21.25 12.875C21.25 7.775 17.1 3.625 12 3.625Z" fill = "currentColor" />
< path d = "M16 13.625H8C7.59 13.625 7.25 13.285 7.25 12.875C7.25 12.465 7.59 12.125 8 12.125H16C16.41 12.125 16.75 12.465 16.75 12.875C16.75 13.285 16.41 13.625 16 13.625Z" fill = "currentColor" />
< path d = "M12 17.625C11.59 17.625 11.25 17.285 11.25 16.875V8.875C11.25 8.465 11.59 8.125 12 8.125C12.41 8.125 12.75 8.465 12.75 8.875V16.875C12.75 17.285 12.41 17.625 12 17.625Z" fill = "currentColor" />
</ svg >
< span class = "hfilter-chip-label" >Tags</ span >
< span class = "hfilter-chip-values" >
< span class = "hfilter-chip-value" >3 selected</ span >
</ span >
</ button >
< div class = "hpopup" id = "tags-filter" popover style = "position-anchor: --tags-filter" >
< ul class = "hlistbox" role = "listbox" aria-label = "Tags" aria-multiselectable = "true" tabindex = "-1" >
< li class = "hoption" id = "tag-refund" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Refund</ span >
</ li >
< li class = "hoption" id = "tag-invoice" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Invoice</ span >
</ li >
< li class = "hoption" id = "tag-bug" role = "option" aria-selected = "true" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Bug report</ span >
</ li >
< li class = "hoption" id = "tag-feature" role = "option" aria-selected = "false" >
< span class = "hoption-checkbox" aria-hidden = "true" ></ span >
< span class = "hoption-label" >Feature request</ span >
</ li >
</ ul >
< div class = "hlistbox-footer" >
< button class = "hbutton" data-variant = "tertiary" type = "button" >
< span class = "hbutton-label" >Clear filters</ span >
</ button >
</ div >
</ div >
View full source
Text filter A chip can open a small panel instead of a list, such as a text filter. The popup has no padding, so the panel lays itself out with style here. Type a name and press Apply or Enter .
const id = useId ();
const popup = useRef < HTMLDivElement >( null );
const input = useRef < HTMLInputElement >( null );
const [ text , setText ] = useState ( "" );
const apply = () => {
setText ( input . current ?. value . trim () ?? "" );
popup . current ?. hidePopover ();
};
return (
< Popup
ref = { popup }
id = { id }
trigger = {
< FilterChip label = "Name contains" values = { text ? [text] : undefined } />
}
>
< div style = { { display : "grid" , gap : 16 , padding : 12 } } >
< Field label = "Name contains" >
< Input
ref = { input }
name = "name"
type = "text"
onKeyDown = { ( event ) => {
if ( event .key !== "Enter" ) return ;
event . preventDefault ();
apply ();
} }
/>
</ Field >
< Button
popoverTarget = { id }
popoverTargetAction = "hide"
onClick = { apply }
>
Apply
</ Button >
</ div >
</ Popup >
);
View full source
< button
class = "hfilter-chip"
type = "button"
aria-expanded = "false"
popovertarget = "name-filter"
style = "anchor-name: --name-filter"
>
< svg class = "hicon" width = "18" height = "18" viewBox = "0 0 24 25" fill = "none" aria-hidden = "true" focusable = "false" >
< path d = "M12 23.625C6.07 23.625 1.25 18.805 1.25 12.875C1.25 6.945 6.07 2.125 12 2.125C17.93 2.125 22.75 6.945 22.75 12.875C22.75 18.805 17.93 23.625 12 23.625ZM12 3.625C6.9 3.625 2.75 7.775 2.75 12.875C2.75 17.975 6.9 22.125 12 22.125C17.1 22.125 21.25 17.975 21.25 12.875C21.25 7.775 17.1 3.625 12 3.625Z" fill = "currentColor" />
< path d = "M16 13.625H8C7.59 13.625 7.25 13.285 7.25 12.875C7.25 12.465 7.59 12.125 8 12.125H16C16.41 12.125 16.75 12.465 16.75 12.875C16.75 13.285 16.41 13.625 16 13.625Z" fill = "currentColor" />
< path d = "M12 17.625C11.59 17.625 11.25 17.285 11.25 16.875V8.875C11.25 8.465 11.59 8.125 12 8.125C12.41 8.125 12.75 8.465 12.75 8.875V16.875C12.75 17.285 12.41 17.625 12 17.625Z" fill = "currentColor" />
</ svg >
< span class = "hfilter-chip-label" >Name contains</ span >
</ button >
< div class = "hpopup" id = "name-filter" popover style = "position-anchor: --name-filter" >
< div style = "display: grid; gap: 16px; padding: 12px" >
< div class = "hfield" >
< label class = "hlabel" for = "name-filter-value" >Name contains</ label >
< input class = "hinput" id = "name-filter-value" name = "name" type = "text" />
</ div >
< button
class = "hbutton"
type = "button"
popovertarget = "name-filter"
popovertargetaction = "hide"
>
< span class = "hbutton-label" >Apply</ span >
</ button >
</ div >
</ div >
View full source
Disabled disabled dims the chip and stops it opening.
< FilterChip label = "Assignee" disabled /> < button class = "hfilter-chip" type = "button" disabled >
< svg class = "hicon" width = "18" height = "18" viewBox = "0 0 24 25" fill = "none" aria-hidden = "true" focusable = "false" >
< path d = "M12 23.625C6.07 23.625 1.25 18.805 1.25 12.875C1.25 6.945 6.07 2.125 12 2.125C17.93 2.125 22.75 6.945 22.75 12.875C22.75 18.805 17.93 23.625 12 23.625ZM12 3.625C6.9 3.625 2.75 7.775 2.75 12.875C2.75 17.975 6.9 22.125 12 22.125C17.1 22.125 21.25 17.975 21.25 12.875C21.25 7.775 17.1 3.625 12 3.625Z" fill = "currentColor" />
< path d = "M16 13.625H8C7.59 13.625 7.25 13.285 7.25 12.875C7.25 12.465 7.59 12.125 8 12.125H16C16.41 12.125 16.75 12.465 16.75 12.875C16.75 13.285 16.41 13.625 16 13.625Z" fill = "currentColor" />
< path d = "M12 17.625C11.59 17.625 11.25 17.285 11.25 16.875V8.875C11.25 8.465 11.59 8.125 12 8.125C12.41 8.125 12.75 8.465 12.75 8.875V16.875C12.75 17.285 12.41 17.625 12 17.625Z" fill = "currentColor" />
</ svg >
< span class = "hfilter-chip-label" >Assignee</ span >
</ button > API reference React <FilterChip> renders the chip. Given options as children, it also renders the popup and list, shows the chosen labels, and shows Clear filters while a multi-select has values; a single choice closes it. Without children, it's the chip alone, to give a Popup of your own, such as a text filter. Other props are the chip <button>'s attributes.
HTML A <button class="hfilter-chip" type="button">, followed by a .hpopup[popover]. It takes aria-expanded, popovertarget (the popup's id), and style="anchor-name: --…" matching the popup's position-anchor, and aria-haspopup="listbox" when it opens a list. Its parts are the plus as a direct .hicon, .hfilter-chip-label, .hfilter-chip-values, and .hfilter-chip-value. Load filter-chip.css and popup.css; listbox.css for a list; button.css for a footer or a panel's action; field.css and input.css for a text panel.
Leave out .hfilter-chip-values until something is chosen, or leave it empty with no whitespace inside, which hides it. Show up to two values; beyond that, one value with a count such as "3 selected". The app supplies the wording. While a multi-select chip has values, end its list with a Clear filters button in a .hlistbox-footer, so the values don't have to be removed one by one. Render the footer only then. It needs a script for the shown values, the Clear filters footer, and closing. See Filter chip's full contract .
Accessibility A <button> with aria-expanded, and aria-haspopup="listbox" when it opens a list. The plus is decorative. The chosen values are part of the button's text, so screen readers hear them with its name: "Status Delivered Read, button, collapsed".