Product
Data toolbar
Search and filter controls for data tables and lists.
$ npx context-ds add data-toolbarUsage
Use it for
Above lists and tables.
Avoid
- A filled search field
- Filters before the search
Dependencies: lucide-react
Example
examples/data-toolbar.tsx
"use client";import * as React from "react";import { DataToolbar, DataToolbarActions, DataToolbarSearch } from "@/components/product/data-toolbar";import { TagFilter } from "@/components/ui/tag-filter";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";export function Product() { const [query, setQuery] = React.useState(""); const [status, setStatus] = React.useState("all"); return ( <DataToolbar className="w-full max-w-2xl"> <DataToolbarSearch value={query} onValueChange={setQuery} placeholder="Search keys" label="Search API keys" /> <DataToolbarActions> <Select value={status} onValueChange={setStatus}> <SelectTrigger appearance="ghost" aria-label="Status"> <SelectValue /> </SelectTrigger> <SelectContent align="end"> <SelectItem value="all">All statuses</SelectItem> <SelectItem value="active">Active</SelectItem> <SelectItem value="disabled">Disabled</SelectItem> </SelectContent> </Select> </DataToolbarActions> </DataToolbar> );}export function Collapsible() { const [query, setQuery] = React.useState(""); const [tags, setTags] = React.useState<string[]>(["pricing"]); return ( <DataToolbar className="w-full max-w-2xl"> <DataToolbarSearch collapsible value={query} onValueChange={setQuery} placeholder="Search monitors" /> <DataToolbarActions> <TagFilter tags={tags} tagOptions={["pricing", "docs", "competitors"]} onAddTag={(t) => setTags((all) => [...all, t])} onRemoveTag={(t) => setTags((all) => all.filter((x) => x !== t))} /> </DataToolbarActions> </DataToolbar> );}Source
components/product/data-toolbar.tsx
"use client";import * as React from "react";import { Search, X } from "lucide-react";import { cn } from "@/lib/utils";// Above a table: the search at the left, the filters (ghost selects, tags) at the right, one line that wraps only// when it has to. The search is a 32px bordered field 256px wide; `collapsible` keeps it a 36px icon until it's// opened, then a field ringed in blue that Escape or its close button clears and folds away.function DataToolbar({ className, children, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="data-toolbar" className={cn("flex flex-wrap items-center justify-between gap-x-4 gap-y-2", className)} {...props}> {children} </div> );}function DataToolbarSearch({ value, onValueChange, placeholder = "Search", label, collapsible = false, className, children,}: { value: string; onValueChange: (value: string) => void; placeholder?: string; /** The field's accessible name; the placeholder when left out. */ label?: string; collapsible?: boolean; className?: string; /** Controls inside the open field, before its close button (search parameters). */ children?: React.ReactNode;}) { const [open, setOpen] = React.useState(value !== ""); const inputRef = React.useRef<HTMLInputElement>(null); const focusOnOpen = React.useRef(false); React.useEffect(() => { if (!open || !focusOnOpen.current) return; focusOnOpen.current = false; inputRef.current?.focus(); }, [open]); const close = () => { onValueChange(""); setOpen(false); }; const name = label ?? placeholder; if (collapsible && !open && !value) { return ( <button type="button" aria-label={name} onClick={() => { focusOnOpen.current = true; setOpen(true); }} className={cn("grid size-9 shrink-0 place-items-center rounded-control text-fg-subtle outline-none transition-colors duration-150 hover:bg-wash-hover hover:text-fg focus-visible:ring-2 focus-visible:ring-focus-ring", className)} > <Search className="size-4" strokeWidth={1.5} /> </button> ); } if (collapsible) { return ( <search data-slot="data-toolbar-search" className={cn("flex h-9 w-64 min-w-0 shrink items-center gap-2 rounded-control border border-brand pr-1 pl-3.5 shadow-[0_0_0_4px_var(--ds-color-blue-10)]", className)}> <Search className="size-3.5 shrink-0 text-fg-subtle" strokeWidth={1.5} /> <input ref={inputRef} type="search" value={value} onChange={(e) => onValueChange(e.target.value)} onKeyDown={(e) => e.key === "Escape" && close()} placeholder={placeholder} aria-label={name} className="min-w-0 flex-1 bg-transparent text-body-sm text-fg outline-none placeholder:text-fg-subtle [&::-webkit-search-cancel-button]:hidden" /> {children} <button type="button" aria-label="Close search" onClick={close} className="grid size-7 shrink-0 place-items-center rounded-sm text-fg-subtle outline-none transition-colors duration-150 hover:text-fg focus-visible:ring-2 focus-visible:ring-focus-ring"> <X className="size-3.5" strokeWidth={1.5} /> </button> </search> ); } return ( <label data-slot="data-toolbar-search" className={cn("flex h-8 w-64 min-w-0 items-center gap-2 rounded-control border border-line px-3 transition-[border-color,box-shadow] duration-150 ease-out focus-within:border-brand hover:border-line-strong motion-reduce:transition-none", className)} > <Search className="size-3.5 shrink-0 text-fg-subtle" strokeWidth={1.5} /> <input type="search" value={value} onChange={(e) => onValueChange(e.target.value)} placeholder={placeholder} aria-label={name} className="min-w-0 flex-1 bg-transparent text-body-sm text-fg outline-none placeholder:text-fg-subtle" /> {children} </label> );}/** The filters at the right end of the toolbar, 4px apart. */function DataToolbarActions({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="data-toolbar-actions" className={cn("ml-auto flex flex-wrap items-center gap-1", className)} {...props} />;}export { DataToolbar, DataToolbarActions, DataToolbarSearch };Reference: context-dashboard c0ddf7a src/components/api-keys/api-keys-page.tsx.