# Chip

A compact, rounded label for a value someone chose, such as a tag, a recipient or a filter, which they may remove.

- **Import:** `import { Chip, ChipGroup } from "@booleanpress/ui/chip"`
- **Page:** <https://ui.booleanpress.com/components/chip> · @booleanpress/ui 0.2.0

## Usage

```tsx
import { Chip, ChipGroup } from "@booleanpress/ui/chip"

export function Recipients({ list, remove }: { list: string[]; remove: (address: string) => void }) {
  return (
    <ChipGroup aria-label="Recipients">
      {list.map((address) => (
        <Chip key={address} label={address} onRemove={() => remove(address)} />
      ))}
    </ChipGroup>
  )
}
```

`label` is the text. `icon` puts a 14 px icon before it; `image` a round 22 px picture (`imageAlt` is empty by default, because the label names the chip). `onRemove` adds a remove button named "Remove {label}" (the provider's `removeItem`); the chip does not remove itself, so take it out of your list in the handler.

`ChipGroup` lays chips out in a wrapping row and makes them a list. When a chip is removed, focus moves to the next chip's remove button, or the previous one's, or the group when none is left, so a keyboard user can keep removing. Name the group with `aria-label` or `aria-labelledby`. A chip is not a button and not a toggle: for a choice that stays pressed, use a toggle group.

## Examples

### Basic

A row of labels.

```tsx
import { Chip } from "@booleanpress/ui/chip"

export default function ChipBasic() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Chip label="Transactional" />
      <Chip label="Marketing" />
      <Chip label="Password resets" />
    </div>
  )
}
```

### Icon

`icon` puts a 14 px icon before the label.

```tsx
import { GlobeIcon, KeyRoundIcon, MailIcon, ServerIcon } from "lucide-react"
import { Chip } from "@booleanpress/ui/chip"

export default function ChipIcon() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Chip icon={<MailIcon />} label="SMTP" />
      <Chip icon={<ServerIcon />} label="Amazon SES" />
      <Chip icon={<GlobeIcon />} label="mail.example.com" />
      <Chip icon={<KeyRoundIcon />} label="API key" />
    </div>
  )
}
```

### Image

`image` puts a round picture before the label.

```tsx
import { Chip } from "@booleanpress/ui/chip"

function portrait(fill: string, face: string) {
  return (
    "data:image/svg+xml;utf8," +
    encodeURIComponent(
      `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" fill="${fill}"/><circle cx="32" cy="25" r="11" fill="${face}"/><path d="M10 64c2-16 12-24 22-24s20 8 22 24z" fill="${face}"/></svg>`
    )
  )
}

export default function ChipImage() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Chip image={portrait("#6366f1", "#e0e7ff")} label="Ada Lovelace" />
      <Chip image={portrait("#0ea5e9", "#e0f2fe")} label="Grace Hopper" />
      <Chip image={portrait("#f97316", "#ffedd5")} label="Alan Turing" />
    </div>
  )
}
```

### Removable

`onRemove` adds a remove button; Backspace and Delete remove the chip too.

```tsx
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import { Chip, ChipGroup } from "@booleanpress/ui/chip"

const TAGS = ["Billing", "Refund", "Priority", "VIP"]

export default function ChipRemovable() {
  const [tags, setTags] = useState(TAGS)

  return (
    <div className="flex flex-col items-center gap-3">
      {/* In a group, focus lands on the group when the last chip goes, not on the page. */}
      <ChipGroup aria-label="Ticket tags" className="justify-center">
        {tags.map((tag) => (
          <Chip key={tag} label={tag} onRemove={() => setTags((current) => current.filter((t) => t !== tag))} />
        ))}
      </ChipGroup>
      {tags.length < TAGS.length ? (
        <Button variant="link" size="sm" onClick={() => setTags(TAGS)}>
          Restore tags
        </Button>
      ) : null}
    </div>
  )
}
```

### Group

`ChipGroup` makes a named list; removing a chip moves focus to the next one.

```tsx
import { useState } from "react"
import { Chip, ChipGroup } from "@booleanpress/ui/chip"

const RECIPIENTS = ["ops@example.com", "billing@example.com", "support@example.com", "alerts@example.com"]

export default function ChipGroupExample() {
  const [recipients, setRecipients] = useState(RECIPIENTS)

  return (
    <div className="flex w-full max-w-sm flex-col gap-2">
      <p id="chip-group-recipients" className="text-sm font-medium">
        Failure alerts go to
      </p>
      <ChipGroup aria-labelledby="chip-group-recipients">
        {recipients.map((address) => (
          <Chip
            key={address}
            label={address}
            onRemove={() => setRecipients((current) => current.filter((a) => a !== address))}
          />
        ))}
      </ChipGroup>
      {recipients.length === 0 ? <p className="text-sm text-muted-foreground">No one gets failure alerts.</p> : null}
    </div>
  )
}
```

### Disabled

`disabled` dims the chip and turns its remove button off, beside a chip that can still be removed.

```tsx
import { Chip, ChipGroup } from "@booleanpress/ui/chip"

export default function ChipDisabled() {
  return (
    <ChipGroup aria-label="Ticket tags">
      <Chip label="Billing" onRemove={() => {}} />
      <Chip label="Refund" disabled onRemove={() => {}} />
    </ChipGroup>
  )
}
```

## Accessibility

**Semantics.** A `div`; in a `ChipGroup` (`role="list"`) each chip is a `listitem`. The remove button is a native `button` whose 22 px circle takes presses over a 24 px square, the minimum target of WCAG 2.5.8; the icon and the image are decorative unless you give `imageAlt`.

**Labels.** The remove button is named from the provider's `removeItem` string with the label filled in: "Remove billing@example.com".

**Focus.** A chip without a remove button is not focusable. A removable chip's remove button is its tab stop, and the chip turns one step darker while the button has keyboard focus. After a removal, focus goes to the next chip's remove button, else the previous one's, else the group.

**Known limits.**

- The chip calls `onRemove` and moves focus at once; it does not wait for your list to change. If you ask before removing, move focus yourself.
- A disabled chip's remove button leaves the tab order, and focus skips it after a removal.
- Outside a `ChipGroup` focus can only move to a neighbouring chip in the same parent; when the last removable chip there goes, focus has nowhere to land. Put removable chips in a `ChipGroup`.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Moves to the next removable chip's remove button. |
| Enter | On the remove button: removes the chip and moves focus to the next chip, or the previous one. |
| Space | On the remove button: removes the chip and moves focus to the next chip, or the previous one. |
| Backspace | On the remove button: removes the chip and moves focus to the next chip, or the previous one. |
| Delete | On the remove button: removes the chip and moves focus to the next chip, or the previous one. |

## API

### Chip

Renders a `div` and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `string` |  | The chip's text; the remove button is named after it. |
| `disabled` | `boolean` | `false` | Dims the chip and disables its remove button. |
| `icon` | `ReactNode` |  | An icon before the label, drawn at 14px. |
| `image` | `string` |  | The address of a round image before the label, such as a person's photo. |
| `imageAlt` | `string` |  | The image's text alternative; empty (decorative) by default, as the label names the chip. |
| `onRemove` | `(() => void)` |  | Shows a remove button, named "Remove {label}", which calls this; Backspace and Delete on it call it too. |

### ChipGroup

Renders a `div` and passes it every other prop.

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="chip"` (Chip), `data-slot="chip-group"` (ChipGroup), and `data-removable`, `data-disabled`.

**Provider strings:** `removeItem` (`BooleanUIProvider`'s `strings`).

## Theming

A chip fills with `--secondary` (`--secondary-hover` while its remove button has focus) and draws its text and icons in `--accent-foreground`.

| Token | Used for |
| --- | --- |
| `--accent-foreground` | text |
| `--ring` | outline |
| `--secondary` | background |
| `--secondary-hover` | background |
