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.
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.
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.
| Component | Description | Import from |
|---|---|---|
| FilterBar.Root | The 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.Content | The 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.Actions | The 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.FilterRow | One 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.FilterIconButton | A 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 |
| FilterDropdown | The 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 |
| FilterDialog | The 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 |
| CountBadge | The 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 |
|---|---|
| Button | The default Filters trigger at sm secondary, and the menu footer’s xs buttons: secondary Add filter and Clear all, primary Apply filter. |
| CloseButton | The 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/icons | FilterLines 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.
Eight drawn types, eight composed here
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
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
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.
What changes | Desktop | Mobile |
|---|---|---|
| The menu’s surface | A 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 shape | Field, 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 state | Copy 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 type | Rows 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 itself | Content 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
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.
The border counts inside the 20×22
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”.States
Hover, focus, open, applied, and disabled belong to the trigger and buttons; the menu has two bodies, empty and rows.
Disabled and focused match the sheet exactly
#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.