ComponentsMisc
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"Usage
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.
Icon
icon puts a 14 px icon before the label.
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.
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.
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.
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.
Accessibility
- Semantics
- A
div; in aChipGroup(role="list") each chip is alistitem. The remove button is a nativebuttonwhose 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 giveimageAlt. - Labels
- The remove button is named from the provider's
removeItemstring 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
onRemoveand 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
ChipGroupfocus 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 aChipGroup.
- The chip calls
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 |
|---|---|---|---|
labelrequired | 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.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent-foreground | text |
--ring | outline |
--secondary | background |
--secondary-hover | background |