Tessera UI

Chip

Interactive label pills for filtering, tagging, and selection. Four variants, five intents, five sizes, six rounded options, optional remove button, leading icon/avatar/status slots, and RTL support.

Installation

pnpm add @tessinaui/ui

Usage

import { Chip, ChipGroup } from "@tessinaui/ui";
<ChipGroup>
  <Chip>Label</Chip>
  <Chip selected>Selected</Chip>
  <Chip onRemove={() => {}}>Removable</Chip>
  <Chip leadingIcon={<Tag />}>With icon</Chip>
</ChipGroup>

Playground

Preview
Loading...

Showcase

Preview
Loading...

Variants

VariantDescription
outlineBorder + white background. Default. Matches Figma spec.
softFilled with a subtle secondary background. No border.
solidFilled with the intent colour (or secondary for none).
ghostNo border, no background. Minimal emphasis.

API Reference

Chip

PropTypeDefaultDescription
variant"outline" | "soft" | "solid" | "ghost""outline"Visual fill style
intent"none" | "error" | "warning" | "success" | "info""none"Semantic colour — applied when selected
size"xs" | "sm" | "md" | "lg" | "xl""md"Text size and padding
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Border radius
selectedbooleanfalseActive/toggled state — fills with intent colour
disabledbooleanfalseDisables interaction
dir"ltr" | "rtl""ltr"Text direction
leadingIconReactNodeIcon before label (controlled by showLeadingIcon)
leadingAvatarChipAvatarDefAvatar before label — picks one leading slot
leadingStatusStatusVariantColoured status dot before label
showLeadingIconbooleantrueShow or hide leadingIcon / leadingStatus
trailingIconReactNodeIcon after label (ignored when onRemove is provided)
onRemove(e: MouseEvent) => voidRenders an × remove button when provided
onClickMouseEventHandlerClick handler — typically used to toggle selected
classNamestringExtra classes on the root element

ChipAvatarDef

FieldTypeDescription
srcstringAvatar image URL
initialsstring1-2 char fallback when no src
statusStatusVariantStatus dot on the avatar (online/away/busy/…)

ChipGroup

Container that wraps multiple chips with consistent spacing.

PropTypeDefaultDescription
gap"sm" | "md" | "lg""md"Gap between chips (4px / 6px / 8px)
wrapbooleantrueWrap chips to multiple lines
dir"ltr" | "rtl""ltr"Text direction

Leading Slot Priority

When multiple leading props are provided, the first match wins:

  1. leadingAvatar (avatar always renders if defined)
  2. leadingStatus (status dot — requires showLeadingIcon !== false)
  3. leadingIcon (icon — requires showLeadingIcon !== false)

Remove Button

When onRemove is provided, the chip renders as two sibling <button> elements inside a pill container:

<Chip onRemove={(e) => removeTag(tag)}>
  TypeScript
</Chip>

The remove button fires onRemove and stops click propagation so it does not trigger onClick (the toggle handler).

Toggle Pattern

const [selected, setSelected] = useState(false);

<Chip
  selected={selected}
  onClick={() => setSelected((s) => !s)}
>
  Filter
</Chip>

Filter Group

const [active, setActive] = useState<string[]>([]);

function toggle(tag: string) {
  setActive((prev) =>
    prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
  );
}

<ChipGroup>
  {tags.map((tag) => (
    <Chip
      key={tag}
      selected={active.includes(tag)}
      onClick={() => toggle(tag)}
    >
      {tag}
    </Chip>
  ))}
</ChipGroup>

Accessibility

  • Renders as <button type="button"> with aria-pressed reflecting the selected state.
  • When onRemove is provided, the main area and remove button are separate focusable <button> elements — fully keyboard navigable.
  • The remove button has aria-label="Remove".
  • disabled applies pointer-events-none and opacity-50 to all interactive areas.
  • Touch targets meet WCAG 2.1 AA (≥ 44×44 px) at md, lg, xl sizes via min-h-[44px].
  • Leading icon and trailing icon containers have aria-hidden="true" — labels provide the accessible text.

On this page