# Filter Chip

An applied filter showing its field and value, with a remove button.

Source: [filter-chip.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/filter-chip.tsx)

```tsx
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>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/filter-chip
```

## Usage

```tsx
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

- `FilterChip`
  - `FilterChipLabel`
  - `FilterChipValue`
  - `FilterChipRemove`

## Examples

### Without a remove button

Leave out `FilterChipRemove` for a filter that can't be removed.

```tsx
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.

```tsx
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](https://ui.jarv.is/components/button.md) except `variant` and `size`.

| Part | Extends |
| --- | --- |
| `FilterChip` | `<span>` |
| `FilterChipLabel` | `<span>` |
| `FilterChipValue` | `<span>` |
| `FilterChipRemove` | [`Button`](https://ui.jarv.is/components/button.md) |

### FilterChipRemove

| Prop                     | Type      | Description                                                                                                                                                                                       |
| ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `nativeButton?`          | `boolean` | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Default: `true` |
| `focusableWhenDisabled?` | `boolean` | Whether the button should be focusable when disabled. Default: `false`                                                                                                                            |
