Filters

Narrow a list without leaving it: FilterBar’s five layout parts plus the menu’s dropdown, dialog, and count badge. Row inputs and all state are yours.

Demo

FilterDropdown’s own controls, plus two of the demo’s: which surface to draw, and what it opens holding.

Rows in the menu
appliedCount
surface
placement
import { Calendar, SearchLg } from "@untitledui/icons"; import { Button } from "@/components/base/buttons/button"; import { FilterBar } from "@/components/application/filter-bar/filter-bar"; import { FilterDropdown } from "@/components/application/filter-bar/filter-dropdown-menu"; <FilterBar.Root> <FilterBar.Content> <Button size="sm" color="secondary" iconLeading={Calendar}> Last 30 days </Button> </FilterBar.Content> <FilterBar.Actions> <FilterBar.FilterIconButton icon={SearchLg} label="Search orders" /> <FilterDropdown filters={filters} appliedCount={3} onApply={(next) => setApplied(next.length)} onAddFilter={addRow} onRemoveFilter={removeRow} onFilterChange={patchRow} onClearAll={clearAll} renderFilterRow={(filter, onChange) => ( <FieldOperatorValueInputs filter={filter} onChange={onChange} /> )} /> </FilterBar.Actions> </FilterBar.Root>

surface is the kit’s prop now

auto is the sheet’s rule — the menu from 768px up, the slideout below — and at this width it draws the menu inline. slideout pins the panel on any width, which is how the sheet’s 400px desktop slideout is reached; it is a modal overlay, so the demo frames it at that panel width rather than letting it cover this page. Small screens, further down, shows auto switching in a real 375px viewport.

Two files, one assembly

FilterBar owns the row’s layout and FilterDropdown owns the trigger and its menu; the rows’ field, operator, and value inputs come from your renderFilterRow, and every row lives in your state. The copied snippet carries the whole assembly, because no single element reproduces what the preview shows.

Contextual demo

A filter bar only means something above the list it narrows. Applying a filter here actually filters the rows.

The family

Eight components in two files: five FilterBar layout parts, plus the menu’s three exports and the shared FilterRow type.

The components in this family, with their descriptions and import paths
ComponentDescriptionImport from
FilterBar.RootThe bar itself: a wrapping row with a 12px gap, aligned to the end, holding Content on the left and Actions on the right.@/​components/​application/​filter-bar/​filter-bar
FilterBar.ContentThe left area. Grows to fill the row (flex-1 with a 1px minimum since 2026-08-23), wraps, and aligns its controls to the start.@/​components/​application/​filter-bar/​filter-bar
FilterBar.ActionsThe right area for the date picker, search, and Filters controls. Hugs its content and holds its width — it is shrink-0 — and never wraps internally.@/​components/​application/​filter-bar/​filter-bar
FilterBar.FilterRowOne advanced filter row on the page: a content-hugging inputs group and a 36px remove button. The inputs are yours.@/​components/​application/​filter-bar/​filter-bar
FilterBar.FilterIconButtonA 36px icon-only action styled like a secondary button. The label prop is required, since the button has no text of its own.@/​components/​application/​filter-bar/​filter-bar
FilterDropdownThe trigger and its popover: a Filters button with an optional count badge, opening the dialog 8px below it.@/​components/​application/​filter-bar/​filter-dropdown-menu
FilterDialogThe menu surface: the filter rows, Add filter, Clear all, and Apply filter, or the empty state when there are no rows.@/​components/​application/​filter-bar/​filter-dropdown-menu
CountBadgeThe 20×22 applied-count badge inside the default trigger, with a screen-reader-only “applied” after the number.@/​components/​application/​filter-bar/​filter-dropdown-menu

Built from

Four pieces make up the family; three have their own docs page.

Component
Where it’s used here
ButtonThe default Filters trigger at sm secondary, and the menu footer’s xs buttons: secondary Add filter and Clear all, primary Apply filter.
CloseButtonThe dialog row’s remove button at size sm, with slot={null} so removing a row does not also close the menu. The bar’s FilterRow draws its own remove button instead.
@untitledui/iconsFilterLines and the 16px ChevronDown on the trigger, Plus on Add filter, XClose on the bar row’s remove button.

The bar

All eight types the sheet draws, live and composed from kit parts, in the frame’s canvas order. There is no type prop.

Simple — a 280px search in Content; Select dates and the Filters trigger in Actions. The search flexes under a 280px cap, so at 375px it shrinks the way the sheet’s does
Tabs and search — a button group in Content; search and the trigger in Actions. The sheet hides the chevron here (95px); the kit’s default trigger always draws it
Tabs and date picker — the same group over range tabs, with a 225px date range beside the trigger
Date filters — a preset button, a 128px range select, and a date range in Content; search and the trigger in Actions
Status
Category
Dropdowns — three labelled fields at 280, 160, and 160, and Clear all in Actions. This type draws no Filters trigger at all
Sort by
Dropdowns and date picker — labelled fields with a bottom-aligned Clear all, and a custom “More filters” trigger through children
Advanced inactive filter — the leading 36px icon button, one empty row at 112, 112, 168, and Clear all in Actions
Value
Advanced active filters — three rows wrapping on a 48px stride, the third at 112, 160, 208 with a multi-select value

Eight drawn types, eight composed here

The sheet’s Type axis, in the frame’s canvas order: Simple · Tabs and search · Tabs and date picker · Date filters · Dropdowns · Dropdowns and date picker · Advanced inactive filter · Advanced active filters. Every one is composed above from the kit’s parts plus base components — button groups, selects, inputs, and date buttons the kit ships elsewhere. What the parts do not switch is the per-type layout the completed 2026-08-23 sheet read recorded: the root’s alignment varies by type (most Desktop types items-end, Advanced active items-start, most Mobile types stacking as a column), Content carries a 560px minimum only on the Advanced types and Dropdowns and date picker, and Content wraps only on Advanced. All of it is on the Code tab, and none of it stops a bar from being composed today.

Three things the sheet varies that the default trigger does not

Read from the symbols: Dropdowns draws no Filters trigger at all — its Actions holds one Clear all; Dropdowns and date picker labels its trigger “More filters” at 130px, which needs children and so gives up appliedCount and isDisabled; and the two Tabs types drop the chevron, which puts the trigger at 95px against the kit’s 115. The bar row’s field placeholder is “Filter” here where the menu’s row says “Select filter”. Each is recorded on the Code tab.

The bar row hugs; the menu row flexes

Read from the source: the bar’s FilterRow keeps its inputs group content-hugging and the row shrink-0, because that is what the sheet’s Advanced compositions draw around fixed-width inputs — and the widths are per row, not one set: the two text rows are 112, 112, 168 and the team row is 112, 160, 208. Earlier on 2026-08-23 a partial sheet read had this row flexing — flex-1 min-w-0, the kit’s rendering of the sheet’s flex: 1 0 0; the completed read proved that treatment belongs to the menu’s row only, and the edit was reverted the same day. The dialog’s own row keeps its min-w-0 flex-1 inputs group, which is why the menu fixtures flex and these use fixed widths.

Small screens

The sheet answers mobile with a different surface, not a narrower one — and since 2026-09-02 the kit switches by itself.

Simple at 375px, its trigger pressed: the same FilterDropdown opens the slideout, because below 768px that is what the sheet draws
Advanced at 375px: the rows give way to one full-width Edit filters trigger with the count, composed through children, opening the same slideout
What changes
Desktop
Mobile
The menu’s surfaceA popover under the trigger: 280px empty, 624px with rows from 768px up.A slideout docked to the edge: a 351px panel in a 375px symbol, behind a full-bleed scrim. FilterDropdown makes this switch itself since 2026-09-02. The same panel is drawn 400px wide on desktop, where the sheet paints no scrim — the kit does, at every width.
A row’s shapeField, operator and value on one line, at 160, 160 and 208.Field and operator on one line, the value and its remove button on the next, with a dotted rule between rows.
The empty stateCopy and one Add filter button, with no footer at all.The same copy, and the footer stays: Clear all and Apply filter are always there.
The trigger“Filters” with a 16px chevron, 115px, and 141px once the badge lands.No chevron on Simple and Tabs and date picker (95px); an icon-only 36px button on Tabs and search, Date filters, and Dropdowns and date picker.
The Advanced typeRows on the bar behind a leading icon button, with Clear all in Actions.One full-width button instead of the rows — “Apply filters”, or “Edit filters” with the count inline and the active tint.
The bar itselfContent on the left, Actions on the right, wrapping only when it must.Most types stack as a column, so every control takes the full width in the order it is written.

What the kit switches, and what it leaves to you

Since 2026-09-02 FilterDropdown follows the sheet’s breakpoint on its own: surface="auto" opens the popover from 768px up and the slideout below it, and the default trigger drops its chevron there. FilterBar.Root stacks Content and Actions as full-width lines below 768px. Both frames above are that code in a real 375px viewport. What stays yours is per type: the Advanced bar’s full-width trigger and the icon-only triggers of three types are children compositions, and the flexible search the sheet shrinks to 236px needs min-w-0 flex-1 rather than a fixed width.

The count badge

A 20×22 box beside the trigger label: the number, a screen-reader-only “applied”, and a 1px border the padding absorbs.

1 applied
count={1}
3 applied
count={3}
12 applied
count={12}: the box grows with the digits; nothing caps the number

The border counts inside the 20×22

Closed on 2026-08-23 from the completed sheet read: the sheet’s badge is 20×22 with its 1px stroke drawn inside, so the kit’s padding absorbs the border — px-[5px] py-[1px] where px-1.5 py-0.5 rendered a 22×24 box and made the active trigger 2px wider than the sheet’s 141px. The arithmetic is the sheet’s; the kit’s rendered box is in the pending measured pass. The sr-only “applied” after the number is a 2026-08-23 edit too: without it the trigger’s accessible name ran the label and the bare integer together as “Filters 3”.

The icon button

A 36px icon-only action in the secondary-button recipe. The label is required and lands as aria-label.

icon={SearchLg} label="Search orders"
icon={Calendar} label="Pick a date range"

States

Hover, focus, open, applied, and disabled belong to the trigger and buttons; the menu has two bodies, empty and rows.

Disabled trigger: isDisabled paints the whole button at half opacity
Disabled icon button: isDisabled reaches the element since the 2026-08-23 rest-props edit

Disabled and focused match the sheet exactly

The completed 2026-08-23 sheet read settled both of this family’s open questions. Disabled is a whole-button half opacity with the tokens unchanged, which is the kit’s own treatment. Focused is the sheet’s double shadow — 2px of white then 2px of #3b82f6 — which is exactly the kit’s 2px outline-focus-ring at a 2px offset, with focus-ring resolving to brand-500 in theme.css. Tab to any control above to see it; the ring appears on focus-visible only.