Toolbar React component for Tailwind CSS v4
v1.0.1Updated in 1.2.0
A React toolbar for lists and tables: search, filter chips, applied filters, sort, a list or grid switch, the count, actions and bulk mode.
Type in the search (results update after a short pause, at once on Enter; Escape or × clears it). Tap the filter chips to switch quick filters on and off; when there are more than fit they scroll sideways with snapping and fade at the edges. Applied filters sit on a second row as chips with an ×; Clear filters removes them all. Sort opens a menu of options (arrow keys and Enter, Escape closes); the list / grid switch is a radio group (arrow keys move it). Page actions keep the primary visible and fold the rest into ⋯ as the row narrows. Select rows and the bar turns into a selection bar: the count, the bulk actions (folding into ⋯ when they do not fit) and × to clear, which Escape also does while focus is in the toolbar or the list next to it. On a phone the toolbar takes two rows (search with the primary action and ⋯, then the chips with sort and view as icon buttons), menus open as bottom sheets, every target is 44px, and the selection bar floats as an inverted pill above the tab bar and the home indicator. Sticky toolbars pin under the top bar on glass and show a hairline once stuck.
Toolbar is a Pro React component in the React layout components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add toolbar, together with Badge, Button, Dropdown Menu, Search Input, Segmented Control and 9 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add toolbarToolbar is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import Toolbar from @/components/ui/toolbar.
import { useState } from "react"
import { Archive, Download, Plus, Trash2, Upload } from "lucide-react"
import { Toolbar } from "@/components/ui/toolbar"
export function ClientsToolbar() {
const [query, setQuery] = useState("")
const [active, setActive] = useState<string[]>(["active"])
const [sort, setSort] = useState("newest")
const [view, setView] = useState("list")
const [selected, setSelected] = useState<string[]>([])
return (
<Toolbar
searchPlaceholder="Search clients"
searchValue={query}
onSearchChange={setQuery}
filters={[
{ id: "active", label: "Active", active: active.includes("active") },
{ id: "vip", label: "VIP", active: active.includes("vip") },
{ id: "overdue", label: "Overdue", count: 3, active: active.includes("overdue") },
]}
onFilterToggle={(id, on) => setActive((list) => (on ? [...list, id] : list.filter((x) => x !== id)))}
sort={{
label: "Sort",
value: sort,
onValueChange: setSort,
options: [
{ value: "newest", label: "Newest" },
{ value: "name", label: "Name (A-Z)" },
{ value: "balance", label: "Highest balance" },
],
}}
view={view}
onViewChange={setView}
resultCount={48}
locale="en-US"
actions={[
{ label: "Import CSV", icon: <Upload /> },
{ label: "Export", icon: <Download /> },
{ label: "New client", icon: <Plus />, primary: true },
]}
selectedCount={selected.length}
bulkActions={[
{ label: "Archive", icon: <Archive /> },
{ label: "Delete", icon: <Trash2 />, tone: "danger" },
]}
onClearSelection={() => setSelected([])}
/>
)
}Props
searchdefaulttruebooleanShows the search field (a SearchInput): full width on phones, 224 to 288px on wider toolbars.
searchPlaceholderdefault"Search"stringThe search field's placeholder and accessible name.
searchShortcutdefault""stringA key that focuses the search from anywhere, shown as a hint in the empty field (never on touch). None by default: "/" belongs to the app's global search.
searchValuestringThe controlled query. Typing stays local; the field follows this value whenever it changes.
defaultSearchValuedefault""stringThe starting query while uncontrolled.
onSearchChange(query: string) => voidThe trimmed query, debounced by searchDebounce; at once on Enter and when the field is cleared.
searchDebouncedefault300numberMilliseconds of quiet before onSearchChange runs.
searchPropsPartial<SearchInputProps>Anything else for the SearchInput: suggestions, recent searches, variant, an aria-label.
filtersToolbarFilter[]Quick toggle chips { id, label, icon?, count?, active?, disabled? }: outline pills with aria-pressed and an optional count. active makes a chip controlled; without it the chip keeps its own state. They scroll sideways with snapping when they do not fit.
onFilterToggle(id: string, active: boolean) => voidRuns when a chip is switched on or off.
filterTriggerReactNodeA slot after the chips for a "Filters" button with its own panel (a dropdown, a popover, a filter panel).
appliedFiltersToolbarAppliedFilter[]{ id, label }[]: removable chips on a second row. They enter and leave with a spring and the rest slide into place.
onRemoveFilter(id: string) => voidCalled by an applied chip's × (and Backspace or Delete on it). Without it the chips have no ×.
onClearFilters() => voidShows the Clear filters link after the applied chips.
clearLabeldefault"Clear filters"stringThe text of the link that removes every applied filter.
sortToolbarSort{ value, options: { value, label }[], onValueChange, label? }: a ghost "Sort: Newest" button opening a radio menu (an icon button on narrow toolbars and phones, where the menu is a bottom sheet).
viewstringThe controlled view (list / grid). A value that is not in views checks no segment.
defaultViewdefault"list"stringThe starting view while uncontrolled.
onViewChange(view: string) => voidRuns when the view switch changes.
viewsToolbarView[]{ value, label, icon }[] for the icon-only segmented switch; labels become tooltips and accessible names. Default list and grid; fewer than two hide the switch.
viewPersistKeydefault""stringRemembers the uncontrolled view in localStorage under this key (use-persisted-state), shared by every toolbar with the same key.
resultCountdefault0number"48 results" (through labels.results and the locale) before the actions, announced politely. Shown on toolbars wider than 896px; leave it out to hide the count.
actionsToolbarAction[] | ReactNode{ id?, label, icon?, onSelect?, variant?, tone?, primary?, disabled? }[], the page-header shape: the primary (or the last solid) stays visible at the end, the others are outline buttons that fold into ⋯ from the end. On phones the primary is an icon button and the rest open from ⋯ as an action sheet. onSelect returning a promise shows the button loading. A node renders as is.
selectedCountdefault0numberAbove 0 the toolbar becomes the selection bar: "3 selected", the bulk actions and ×.
bulkActionsToolbarAction[]Ghost buttons in the selection bar; they fold into ⋯ when they do not fit, danger keeps its red, and in the floating bar on phones they turn icon-only.
onClearSelection() => voidThe selection bar's × and Escape (while focus is in the toolbar, the floating bar or the list next to the toolbar).
bulkPositiondefault"inline""inline" | "floating"inline swaps the toolbar's row for the selection bar; floating shows an inverted pill (bg-primary) pinned to the bottom of the screen, above var(--bottom-bar-offset) and the safe area. Below 768px it always floats.
sizedefault"md""sm" | "md"Controls 32 / 40px (44px on touch); the chips are a step shorter (28 / 32px with a 44px hit area).
variantdefault"plain""plain" | "surface"plain sits on the page; surface is a raised rounded-2xl panel with 8px padding.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"The radius of the search field, the buttons, the view switch and the selection band, per size like the button's.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The color of pressed filter chips (a soft tint with a tone border).
colordefault""stringAny CSS color for pressed filter chips; replaces the tone. Leave empty to use the tone.
stickydefaultfalsebooleanSticks under the top bar (top: var(--topbar-offset, 0px)) on glass, with a hairline that appears once it is stuck.
loadingdefaultfalsebooleanThe search icon turns into a spinner and the field gets aria-busy.
localedefault""stringFormats the result count, the selected count and chip counts (1,250 in en-US). Default: the app's locale.
labelsPartial<ToolbarLabels>Every string: region ("List controls"), results ((n, formatted) => "48 results"), selected ((n, formatted) => "3 selected"), sort ("Sort"), view ("View"), more ("More actions"), clearSelection ("Clear selection"), filters ("Quick filters"), appliedFilters ("Applied filters"), removeFilter ("Remove filter"), bulkActions ("Selection actions"), cancel ("Cancel", the phone sheets).
childrenReactNodeExtra controls after the view switch (a density toggle, a columns menu, a select). Below a 48rem toolbar they get their own wrapping row and share its width equally, so they need no fixed widths on phones; from 48rem up they sit inline.
classNamesPartial<Record<ToolbarSlot, string>>Classes for the parts: row, search, filters, filter, filterCount, filterTrigger, sort, view, count, actions, more, applied, appliedFilter, clear, bulk, bulkCount, bulkActions, bulkClear, floating, extra (the row holding children).
Examples
Minimal
A search field and one primary action; no view switch.
Applied filters
Removable chips on a second row that spring out of the way, with a Clear filters link.
Sticky
sticky pins the toolbar under the top bar on glass; the hairline appears once it is stuck. Scroll the frame.
Surface
variant="surface" as a raised panel above a product grid; the switch changes the layout.
Phone
Two rows under 768px, sort and view as icon buttons, and the floating selection bar pinned to the bottom.
Sizes
sm (32px controls) and md (40px, 44px on touch).
Dependencies
Files
- components/ui/toolbar.tsx
FAQ
Is the React Toolbar component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Toolbar?
Run npx wingo-ui@latest add toolbar. It also adds the Wingo UI items it builds on (Badge, Button, Dropdown Menu, Search Input, Segmented Control and 9 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/toolbar.
How does Toolbar behave on phones?
On a phone the toolbar takes two rows (search with the primary action and ⋯, then the chips with sort and view as icon buttons), menus open as bottom sheets, every target is 44px, and the selection bar floats as an inverted pill above the tab bar and the home indicator.
See also
Used in
More React layout components
Articles using this component
- TanStack Table v9 Data Table: Filters, Bulk Actions, Mobile
Component guides12 min read