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

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>

Examples

This site is using your theme

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.

  • Delivered
  • Read
  • Failed
  • Pending
<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>

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.

  • WhatsApp
  • Telegram
  • Email
<FilterChip label="Channel" defaultValue="whatsapp">
  <Option value="whatsapp">WhatsApp</Option>
  <Option value="telegram">Telegram</Option>
  <Option value="email">Email</Option>
</FilterChip>

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.

  • Low
  • Medium
  • High
  • Urgent
<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>

More than two

Beyond two values the chip shows a count instead. The app supplies the wording.

  • Refund
  • Invoice
  • Bug report
  • Feature request
<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>

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>
);

Disabled

disabled dims the chip and stops it opening.

<FilterChip label="Assignee" disabled />

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.

PropEffect
labelThe chip's label, and the list's name
valuesWithout children: the values to show
formatCountThe text shown for more than two values, such as (count) => `${count} selected`. Required with multiple; with values, every value shows without it
multipleLets several options be chosen
value or defaultValueThe chosen value, or an array of them with multiple
onValueChangeCalled with the new value or array
searchLabelAdds a search row, with this as its placeholder and name
emptyMessageRequired with searchLabel: shown when the search matches nothing, such as "No channels found"
clearLabelRequired with multiple: the footer button's text, such as "Clear filters"

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.
HookEffect
.hfilter-chip-values with valuesA divider, then each .hfilter-chip-value as a small gray pill
aria-expanded="true"Open: the hover background stays
disabledDimmed, not-allowed cursor; it can't open

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".