StatusFailedBranchmainAuthorAda Park, Sam Rivera
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
FilterChip,
FilterChipLabel,
FilterChipRemove,
FilterChipValue,
} from "@/components/ui/filter-chip";
const initialFilters = [
{ field: "Status", value: "Failed" },
{ field: "Branch", value: "main" },
{ field: "Author", value: "Ada Park, Sam Rivera" },
];
export default function FilterChipRemovable() {
const [filters, setFilters] = React.useState(initialFilters);
return (
<div role="group" aria-label="Active filters" className="flex flex-wrap items-center gap-2">
{filters.map((filter) => (
<FilterChip key={filter.field}>
<FilterChipLabel>{filter.field}</FilterChipLabel>
<FilterChipValue>{filter.value}</FilterChipValue>
<FilterChipRemove
aria-label={`Remove ${filter.field.toLowerCase()} filter`}
onClick={() => setFilters((list) => list.filter((item) => item !== filter))}
/>
</FilterChip>
))}
{filters.length === 0 && (
<Button variant="ghost" size="sm" onClick={() => setFilters(initialFilters)}>
Reset filters
</Button>
)}
</div>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add filter-chip
pnpm dlx shadcn@latest add @jarvis/filter-chip
Usage
import {
FilterChip,
FilterChipLabel,
FilterChipRemove,
FilterChipValue,
} from "@/components/ui/filter-chip";
export function StatusFilter({ onRemove }: { onRemove: () => void }) {
return (
<FilterChip>
<FilterChipLabel>Status</FilterChipLabel>
<FilterChipValue>Failed</FilterChipValue>
<FilterChipRemove aria-label="Remove status filter" onClick={onRemove} />
</FilterChip>
);
}Give each FilterChipRemove an aria-label that names its field, such as "Remove status filter",
which its tooltip shows too.
Put a row of chips in a role="group" with an aria-label.
Composition
Examples
Without a remove button
Leave out FilterChipRemove for a filter that can't be removed.
DateLast 7 daysEnvironmentProduction
import { FilterChip, FilterChipLabel, FilterChipValue } from "@/components/ui/filter-chip";
export default function FilterChipStatic() {
return (
<div role="group" aria-label="Applied filters" className="flex flex-wrap items-center gap-2">
<FilterChip>
<FilterChipLabel>Date</FilterChipLabel>
<FilterChipValue>Last 7 days</FilterChipValue>
</FilterChip>
<FilterChip>
<FilterChipLabel>Environment</FilterChipLabel>
<FilterChipValue>Production</FilterChipValue>
</FilterChip>
</div>
);
}With a filter menu
A filter bar usually pairs the chips with a menu for adding and editing them.
StatusSucceededMemberAda ParkDateLast 7 days
import { ListFilterIcon } from "lucide-react";
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
FilterChip,
FilterChipLabel,
FilterChipRemove,
FilterChipValue,
} from "@/components/ui/filter-chip";
import { people } from "@/examples/data";
const statuses = [
{ value: "succeeded", label: "Succeeded", dot: "bg-success" },
{ value: "failed", label: "Failed", dot: "bg-destructive" },
{ value: "rejected", label: "Rejected", dot: "bg-warning" },
{ value: "cancelled", label: "Cancelled", dot: "bg-faint" },
];
const members = people.slice(0, 3);
function toggle(list: string[], value: string, checked: boolean) {
return checked ? [...list, value] : list.filter((item) => item !== value);
}
export default function FilterChipFilterMenu() {
const [status, setStatus] = React.useState(["succeeded"]);
const [member, setMember] = React.useState(["ada"]);
return (
<div role="group" aria-label="Active filters" className="flex flex-wrap items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="secondary" />}>
<ListFilterIcon />
Filter
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52">
<DropdownMenuGroup>
<DropdownMenuLabel>Status</DropdownMenuLabel>
{statuses.map((option) => (
<DropdownMenuCheckboxItem
key={option.value}
checked={status.includes(option.value)}
onCheckedChange={(checked) => setStatus(toggle(status, option.value, checked))}
>
<span aria-hidden="true" className={`size-1.5 rounded-full ${option.dot}`} />
{option.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Member</DropdownMenuLabel>
{members.map((person) => (
<DropdownMenuCheckboxItem
key={person.value}
checked={member.includes(person.value)}
onCheckedChange={(checked) => setMember(toggle(member, person.value, checked))}
>
<Avatar size="sm">
<AvatarImage src={person.image} alt="" />
<AvatarFallback>{person.initials}</AvatarFallback>
</Avatar>
{person.name}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
{status.length > 0 && (
<FilterChip>
<FilterChipLabel>Status</FilterChipLabel>
<FilterChipValue>
{statuses
.filter((option) => status.includes(option.value))
.map((option) => option.label)
.join(", ")}
</FilterChipValue>
<FilterChipRemove aria-label="Remove status filter" onClick={() => setStatus([])} />
</FilterChip>
)}
{member.length > 0 && (
<FilterChip>
<FilterChipLabel>Member</FilterChipLabel>
<FilterChipValue>
{members
.filter((person) => member.includes(person.value))
.map((person) => person.name)
.join(", ")}
</FilterChipValue>
<FilterChipRemove aria-label="Remove member filter" onClick={() => setMember([])} />
</FilterChip>
)}
<FilterChip>
<FilterChipLabel>Date</FilterChipLabel>
<FilterChipValue>Last 7 days</FilterChipValue>
</FilterChip>
{status.length + member.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => {
setStatus([]);
setMember([]);
}}
>
Clear
</Button>
)}
</div>
);
}Props
FilterChipRemove takes the props of a Button except variant and size.
| Part | Extends |
|---|---|
| FilterChip | <span> |
| FilterChipLabel | <span> |
| FilterChipValue | <span> |
| FilterChipRemove | Button |
FilterChipRemove
| Prop | Type | Default |
|---|---|---|
nativeButtonWhether the component renders a native | boolean | true |
focusableWhenDisabledWhether the button should be focusable when disabled. | boolean | false |