# Chip

A row of compact, removable entries that shares one value and one tab stop.

Chip is a row of compact entries the user can drop one by one: the tags on a
post, the filters on a search, the people on an invitation. The row is one tab
stop, the arrow keys move inside it, and Backspace removes whatever the user is
standing on.

::component-preview{name="ChipPreview"}
```vue
<template>
  <div class="flex flex-col items-start gap-4">
    <Chip.Group v-model="cities" aria-label="Cities" :class="group">
      <template #default="{ entries, empty }">
        <Chip.Root
          v-for="entry in entries"
          :key="entry.key"
          :class="chip"
          :value="entry.value"
        >
          <svg class="size-4 shrink-0" viewBox="0 0 16 16" aria-hidden="true">
            <path
              d="M8 14s4.5-4.2 4.5-7.5a4.5 4.5 0 1 0-9 0C3.5 9.8 8 14 8 14Z"
              fill="none"
              stroke="currentColor"
              stroke-width="1.5"
              stroke-linejoin="round"
            />
            <circle cx="8" cy="6.5" r="1.5" fill="currentColor" />
          </svg>

          <span class="truncate">{{ entry.label }}</span>

          <Chip.Remove :class="remove">
            <svg class="size-4" viewBox="0 0 16 16" aria-hidden="true">
              <path
                d="m4.5 4.5 7 7m0-7-7 7"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
                stroke-linecap="round"
              />
            </svg>
          </Chip.Remove>
        </Chip.Root>

        <span v-if="empty" class="type-component-sm text-surface-muted">
          Nothing left to remove.
        </span>
      </template>
    </Chip.Group>

    <Button :class="reset" @click="cities = [...defaults]">Reset</Button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Button, Chip } from '@maas/mirror/vue'

const defaults = ['Glasgow', 'Bath', 'Nuremberg', 'Munich']

const cities = ref([...defaults])

const group = 'flex min-h-8 flex-wrap items-center gap-1.5'

const chip = [
  'inline-flex h-8 max-w-full shrink-0 items-center gap-1.5 px-1.5',
  'rounded-component-sm border-2 border-[transparent] type-component-md',
  'bg-primary-subtle text-primary-on-subtle',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-subtle-hover active:bg-primary-subtle-active',
  'data-[disabled=true]:bg-disabled-subtle data-[disabled=true]:text-disabled-on-subtle',
].join(' ')

const remove = [
  'flex size-6 shrink-0 items-center justify-center -mr-1',
  'rounded-component-compact-sm border-2 border-[transparent]',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:opacity-70 active:opacity-50',
].join(' ')

const reset = [
  'inline-flex h-9 items-center justify-center px-3.5',
  'rounded-component-md border-2 border-[transparent] type-component-sm',
  'bg-primary-solid text-primary-on-solid',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:bg-primary-solid-hover active:bg-primary-solid-active',
].join(' ')
</script>
```
::

## Usage guidelines

- Only one kind of chip needs a component of its own: the removable
  collection. The other kinds are compositions of components that already
  exist. The [Chip types](#chip-types) section below has all four.
- Reach for `Chip.Group` when the entries can be dropped and the set is the
  value. When the entries are only a display of a selection made somewhere
  else, use [`Combobox.Chips`](/components/combobox#chips) instead: it reads
  the combobox’s own value and hands focus back to the input.
- The row is one tab stop, so keep it short enough to walk through with the
  arrow keys. Give a long collection a
  [`ScrollArea`](/components/scroll-area) or a search instead.
- The remove buttons sit outside the tab order: Backspace on a
  focused chip is the keyboard route, and a second stop per chip would double
  the length of the row for anyone tabbing through the page.
- Chips ship unstyled. The preview above and the recipes below use the same
  utility classes as the rest of the documentation; there is no chip-specific
  token.

## Anatomy

Assemble the group, one `Chip.Root` per entry the group hands out.

::component-anatomy
---
parts:
  - name: Chip.Group
    required: true
    description: Renders a div with role="group", plus one hidden input per value.
    children:
      - name: Chip.Root
        required: true
        description: Renders a div carrying one value. One per entry.
        children:
          - name: Chip.Remove
            description: Renders a button that drops the chip it sits in.
---
::

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { Chip, Field, Label } from '@maas/mirror/vue'

const tags = ref(['design', 'engineering'])
</script>

<template>
  <Field.Root :options="{ name: 'tags' }">
    <Label :native-label="false">Tags</Label>
    <Chip.Group v-model="tags" v-slot="{ entries }">
      <Chip.Root v-for="entry in entries" :key="entry.key" :value="entry.value">
        {{ entry.label }}
        <Chip.Remove />
      </Chip.Root>
    </Chip.Group>
  </Field.Root>
</template>
```

`Chip.Group` owns the array and hands it back as `entries`, one entry per value,
each carrying the `key` a `v-for` needs and the `label` the chip shows. Loop
over `entries` so the markup follows the value as chips are removed.

## Chip types

Chips come in four kinds. Only one of them is a component here; the other
three are compositions of existing components, styled with the same classes.

::component-preview{name="ChipRecipesPreview"}
```vue
<template>
  <div class="flex flex-col items-start gap-6">
    <div class="flex flex-wrap items-center gap-1.5">
      <Button
        v-for="action in actions"
        :key="action"
        :class="[chip, outline]"
      >
        {{ action }}
      </Button>
    </div>

    <ToggleGroup
      v-model="filters"
      aria-label="Filters"
      class="flex flex-wrap items-center gap-1.5"
      :options="{ multiple: true }"
    >
      <Toggle
        v-for="filter in available"
        :key="filter"
        :class="[chip, translucent]"
        :value="filter"
      >
        {{ filter }}
      </Toggle>
    </ToggleGroup>

    <Chip.Group v-model="people" aria-label="People" :class="group">
      <template #default="{ entries }">
        <Chip.Root
          v-for="entry in entries"
          :key="entry.key"
          :class="[chip, tone]"
          :value="entry.value"
        >
          <Avatar.Root :class="avatar">
            <Avatar.Fallback class="font-medium tracking-[0.02em]">
              {{ initials(entry.label) }}
            </Avatar.Fallback>
          </Avatar.Root>

          <span class="truncate">{{ entry.label }}</span>

          <Chip.Remove :class="remove">
            <svg class="size-4" viewBox="0 0 16 16" aria-hidden="true">
              <path
                d="m4.5 4.5 7 7m0-7-7 7"
                fill="none"
                stroke="currentColor"
                stroke-width="1.5"
                stroke-linecap="round"
              />
            </svg>
          </Chip.Remove>
        </Chip.Root>
      </template>
    </Chip.Group>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Avatar, Button, Chip, Toggle, ToggleGroup } from '@maas/mirror/vue'

const actions = ['Add to calendar', 'Share']
const available = ['Draft', 'Scheduled', 'Published']

const filters = ref(['Draft'])
const people = ref(['Robin Vey', 'Carla Jung'])

function initials(name: string) {
  return name
    .split(' ')
    .map((part) => part.charAt(0))
    .join('')
}

const group = 'flex min-h-8 flex-wrap items-center gap-1.5'

const chip = [
  'inline-flex h-8 max-w-full shrink-0 items-center gap-1.5 px-1.5',
  'rounded-component-sm border-2 type-component-md',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
].join(' ')

const outline = [
  'border-primary-subtle text-primary-on-subtle',
  'hover:border-primary-subtle-hover active:border-primary-subtle-active',
].join(' ')

const translucent = [
  'border-[transparent] bg-primary-subtle text-primary-on-subtle',
  'hover:bg-primary-subtle-hover active:bg-primary-subtle-active',
  'data-[state=on]:bg-primary-solid data-[state=on]:text-primary-on-solid',
  'data-[state=on]:hover:bg-primary-solid-hover',
  'data-[state=on]:active:bg-primary-solid-active',
].join(' ')

const tone = [
  'border-[transparent] bg-primary-muted text-primary-on-muted',
  'hover:bg-primary-muted-hover active:bg-primary-muted-active',
].join(' ')

const remove = [
  'flex size-6 shrink-0 items-center justify-center -mr-1',
  'rounded-component-compact-sm border-2 border-[transparent]',
  'transition-all duration-100 ease-linear',
  'outline-4 outline-transparent focus-visible:focus-ring',
  'hover:opacity-70 active:opacity-50',
].join(' ')

const avatar = [
  'flex size-6 shrink-0 items-center justify-center overflow-hidden',
  'rounded-component-round type-component-3xs bg-primary-solid text-primary-on-solid',
].join(' ')
</script>
```
::

### Assist and suggestion chips

A chip that runs an action or fills something in is a
[`Button`](/components/button) with chip classes. It has no state to hold and
nothing to remove, so a component of its own would add a wrapper and take away
`as`, `asChild` and the loading state.

```vue
<template>
  <Button class="chip" @click="addToCalendar">Add to calendar</Button>
</template>
```

### Filter chips

A chip that holds an on state is a [`Toggle`](/components/toggle), and a row of
them under one value is a [`ToggleGroup`](/components/toggle-group). Paint it
from `data-state`, which is `on` or `off`.

```vue
<template>
  <ToggleGroup v-model="filters" :options="{ multiple: true }">
    <Toggle v-for="filter in available" :key="filter" :value="filter">
      {{ filter }}
    </Toggle>
  </ToggleGroup>
</template>
```

`Chip.Root` takes `asChild`, so a filter chip that also needs the chip data
attributes can wrap a `Toggle` instead of copying its classes.

```vue
<template>
  <Chip.Root as-child label="Draft">
    <Toggle v-model="draft" value="draft">Draft</Toggle>
  </Chip.Root>
</template>
```

### Input chips

A chip standing for something the user picked from a list belongs to the
control that owns the list. `Combobox` ships that anatomy already, and its
chips hand focus back to the input the moment one is dropped, behaviour that
belongs to the combobox rather than to a standalone group.
See [`Combobox.Chips`](/components/combobox#chips).

### Avatar chips

An avatar chip is an ordinary `Chip.Root` with an
[`Avatar`](/components/avatar) at the front of its slot. The chip renders
whatever the slot holds, so a whole component fits where an icon would.

## Examples

### A chip on its own

A single dismissible chip needs no collection. Leave the group out and
`Chip.Root` publishes `data-standalone`, its remove button joins the tab order,
and pressing it emits `remove` with the chip’s value rather than dropping
anything.

```vue
<template>
  <Chip.Root :value="notice.id" :label="notice.title" @remove="dismiss">
    {{ notice.title }}
    <Chip.Remove />
  </Chip.Root>
</template>
```

### Orientation and direction

`orientation` decides which arrow keys move between chips. It is
`'horizontal'` by default, which is how a wrapping row reads; set `'vertical'`
for a stacked list, or `'both'` when the row wraps far enough that either pair
should work. `dir` swaps `ArrowLeft` and `ArrowRight`; unset, it follows a
`DirectionProvider` or the surrounding `dir` attribute, as described in
[Composition](/components/composition).

```vue
<template>
  <Chip.Group v-model="tags" :options="{ orientation: 'both', dir: 'rtl' }" />
</template>
```

### Values that are not strings

`Chip.Group` holds whatever the consumer holds. Three options tell it how to
read an entry: `itemToStringLabel` for the text, `itemToStringValue` for the
key and the submitted form value, and `isItemEqualToValue` for the comparison a
removal runs.

```vue
<script setup lang="ts">
const people = ref([
  { id: 1, name: 'Robin Vey' },
  { id: 2, name: 'Carla Jung' },
])

const options = {
  itemToStringLabel: (entry) => entry.name,
  itemToStringValue: (entry) => String(entry.id),
  isItemEqualToValue: (a, b) => a.id === b.id,
}
</script>

<template>
  <Chip.Group v-model="people" :options="options" />
</template>
```

### Disabling

A chip is disabled when either the group or the chip says so. A disabled chip
is skipped by the arrow keys and refuses Backspace, and its remove button
carries the native `disabled` attribute.

```vue
<template>
  <Chip.Group v-model="tags" v-slot="{ entries }">
    <Chip.Root
      v-for="entry in entries"
      :key="entry.key"
      :disabled="entry.key === 'locked'"
      :value="entry.value"
    >
      {{ entry.label }}
      <Chip.Remove />
    </Chip.Root>
  </Chip.Group>
</template>
```

`readOnly` blocks every removal while keeping the whole row focusable, so the
values can still be read and copied.

### Inside a form

`Chip.Group` renders one visually hidden input per value, all sharing the
group’s `name`, so [the form sees it](/components/form-integration#form-participation). The
submitted value is the string form of the entry, which is what
`itemToStringValue` returns.

```vue
<template>
  <form @submit.prevent="submit">
    <Chip.Group v-model="tags" :options="{ name: 'tags', required: true }" />
  </form>
</template>
```

Without a `name`, from either the group or the field, nothing is submitted at
all. A group rendered outside the `<form>` it belongs to points at it by `id`
through `form`, which reaches every hidden input.

### Styling from state

Paint the chip from `data-focused` and `data-disabled`, and the row from
`data-empty`.

```vue
<template>
  <Chip.Group v-model="tags" class="chips">
    <Chip.Root value="design" class="chip">
      design
      <Chip.Remove class="chip-remove" />
    </Chip.Root>
  </Chip.Group>
</template>

<style>
.chips[data-empty='true'] {
  display: none;
}

.chip[data-disabled='true'] {
  opacity: 0.5;
}
</style>
```

## API reference

**Module.** A bundled `options` object, and `useMirrorChip(id)` as the
programmatic API.

### `Chip.Group`

Renders a `<div role="group">` plus one visually hidden `<input type="hidden">`
per value, and a polite live region that announces removals.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The instance ID. Pass it to reach this group through `useMirrorChip`.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: modelValue
        description: The values in the row. `v-model`.
      - label: Array<ChipValueType>
        escape: true
      - label: undefined
  - items:
      - label: defaultValue
        description: Initial values when uncontrolled.
      - label: Array<ChipValueType>
        escape: true
      - label: '[]'
  - items:
      - label: options
        description: Everything else. Deep-merged over the defaults.
      - label: ChipOptions
      - label: see below
        plaintext: true
---
::

#### Options

::docs-table
---
columns:
  - label: Option
  - label: Type
  - label: Default
rows:
  - items:
      - label: announce
        description: Renders the polite live region and writes a message into it on every removal.
      - label: boolean
      - label: 'true'
  - items:
      - label: removedLabel
        description: Builds the announcement from the removed chip's label.
      - label: '(label: string) => string'
        escape: true
      - label: '`${label} removed`'
        plaintext: true
  - items:
      - label: itemToStringLabel
        description: The text a chip shows. A primitive is its own string; an object shaped like `{ value, label }` answers with the half that fits.
      - label: '(value: ChipValueType) => string'
        escape: true
      - label: defaultItemToStringLabel
        plaintext: true
  - items:
      - label: itemToStringValue
        description: The key a `v-for` holds and the value a hidden input submits.
      - label: '(value: ChipValueType) => string'
        escape: true
      - label: defaultItemToStringValue
        plaintext: true
  - items:
      - label: isItemEqualToValue
        description: How a removal decides which entry it hit. Object values compare by reference unless replaced.
      - label: '(item: ChipValueType, value: ChipValueType) => boolean'
        escape: true
      - label: 'Object.is'
        plaintext: true
  - items:
      - label: name
        description: Form field name, shared by every hidden input.
      - label: string
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: form
        description: The `id` of the form the hidden inputs belong to. Only needed when the group is rendered outside that form.
      - label: string
      - label: undefined
  - items:
      - label: orientation
        description: 'Arrow-key axis. `''both''` moves on either pair.'
      - label: '''horizontal'' | ''vertical'' | ''both'''
      - label: '''horizontal'''
  - items:
      - label: dir
        description: 'Direction for the horizontal arrow keys. Unset, it follows a `DirectionProvider` or the closest `dir` attribute above, then `''ltr''`.'
      - label: '''ltr'' | ''rtl'''
      - label: inherited
        plaintext: true
  - items:
      - label: loop
        description: Arrow keys wrap at the ends.
      - label: boolean
      - label: 'true'
  - items:
      - label: disabled
        description: Disables every chip and blocks removal.
      - label: boolean
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: readOnly
        description: Blocks removal, keeps focus.
      - label: boolean
      - label: inherited from `Field`
        plaintext: true
  - items:
      - label: required
        description: Marks the group required.
      - label: boolean
      - label: inherited from `Field`
        plaintext: true
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: update:modelValue
        description: A chip was removed, or a value was added through the composable.
      - label: Array<ChipValueType>
        escape: true
---
::

#### Slot props

The [field state set](/components/styling#the-field-state-set), plus the three
below.

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: entries
        description: One entry per value, each with `key`, `value` and `label`.
      - label: Array<ChipEntry>
        escape: true
  - items:
      - label: value
        description: The raw array.
      - label: Array<ChipValueType>
        escape: true
  - items:
      - label: empty
        description: Mirrors `data-empty`.
      - label: boolean
---
::

### `Chip.Root`

Renders a `<div>`. Inside a group it is a roving-focus stop and carries a
required `value`; on its own it is a lone chip that emits rather than removing.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: id
        description: The chip’s DOM ID.
      - label: string
      - label: generated
        plaintext: true
  - items:
      - label: value
        description: The value this chip stands for. Required inside a `Chip.Group`, where duplicates throw `duplicate_chip_value`.
      - label: ChipValueType
      - label: undefined
  - items:
      - label: label
        description: The text the remove button names in its `aria-label`. Unset, it comes from `itemToStringLabel`.
      - label: string
      - label: derived
        plaintext: true
  - items:
      - label: disabled
        description: Disables this chip only. A disabled group disables every chip regardless.
      - label: boolean
      - label: 'false'
---
::

#### Emits

::docs-table
---
columns:
  - label: Emit
  - label: Payload
rows:
  - items:
      - label: remove
        description: A lone chip’s remove button was pressed. Never fires inside a group, where the group drops the value itself.
      - label: 'ChipValueType | undefined'
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: value
        description: The chip’s value.
      - label: 'ChipValueType | undefined'
  - items:
      - label: label
        description: The resolved label.
      - label: string
  - items:
      - label: index
        description: 'Mirrors `data-index`. Undefined on a lone chip.'
      - label: 'number | undefined'
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
  - items:
      - label: focused
        description: Mirrors `data-focused`.
      - label: boolean
---
::

### `Chip.Remove`

Renders a `<button type="button">` that drops the chip it sits in. Inside a
group it carries `tabindex="-1"` and the group keeps the tab stop; on a lone
chip it is the tab stop. Throws `missing_context` outside a `Chip.Root`.

`aria-label` defaults to `Remove {label}`, or `Remove` when the chip has no
label. Pass your own to override it.

#### Props

::docs-table
---
columns:
  - label: Prop
  - label: Type
  - label: Default
rows:
  - items:
      - label: disabled
        description: Disables the button on its own. Unset, it follows the chip.
      - label: boolean
      - label: inherited from the chip
        plaintext: true
---
::

#### Slot props

::docs-table
---
columns:
  - label: Prop
  - label: Type
rows:
  - items:
      - label: label
        description: The chip’s label, for wording the button yourself.
      - label: string
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: boolean
---
::

### Composable

`useMirrorChip(id)` reaches a group from anywhere in the app.

::docs-table
---
columns:
  - label: Key
  - label: Type
rows:
  - items:
      - label: value
        description: The raw array.
      - label: ComputedRef<Array<ChipValueType>>
        escape: true
  - items:
      - label: entries
        description: The same array the slot receives.
      - label: ComputedRef<Array<ChipEntry>>
        escape: true
  - items:
      - label: disabled
        description: Mirrors `data-disabled`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: readOnly
        description: Mirrors `data-readonly`.
      - label: ComputedRef<boolean>
        escape: true
  - items:
      - label: add
        description: Appends a value, ignoring one the group already holds.
      - label: '(value: ChipValueType) => void'
        escape: true
  - items:
      - label: remove
        description: Drops a value without moving focus.
      - label: '(value: ChipValueType) => void'
        escape: true
  - items:
      - label: clear
        description: Empties the group.
      - label: () => void
        escape: true
---
::

### Data attributes

`Chip.Group` writes the
[field state set](/components/styling#the-field-state-set).

::data-attributes
::

### CSS variables

::css-variables
::

### Errors

::docs-table
---
columns:
  - label: Code
rows:
  - items:
      - label: missing_chip_value
        description: A `Chip.Root` inside a `Chip.Group` rendered without a `value`.
  - items:
      - label: duplicate_chip_value
        description: Two chips in one group declared the same `value`.
  - items:
      - label: missing_context
        description: 'A `Chip.Remove` rendered outside a `Chip.Root`. A rack-level code, described in [TypeScript](/components/typescript#errors).'
---
::

## Accessibility

The row is `role="group"` and each chip is a focusable element inside it, so
the whole row is one tab stop and the arrow keys move within it. A removal is
announced through a polite live region, so a screen-reader user hears that
the focused chip is gone.

::docs-table
---
columns:
  - label: Key
  - label: Behaviour
rows:
  - items:
      - label: Tab
      - label: Enters the row at the chip that last held focus, else the first enabled one. A second press leaves the row.
        plaintext: true
  - items:
      - label: '`ArrowRight` / `ArrowLeft`'
        plaintext: true
      - label: 'Moves to the next or previous enabled chip. Swapped under `dir="rtl"`, and inert while `orientation` is `''vertical''`.'
        plaintext: true
  - items:
      - label: '`ArrowDown` / `ArrowUp`'
        plaintext: true
      - label: The same, while `orientation` is `'vertical'` or `'both'`.
        plaintext: true
  - items:
      - label: '`Home` / `End`'
        plaintext: true
      - label: Moves to the first or last enabled chip, in either orientation.
        plaintext: true
  - items:
      - label: '`Backspace` / `Delete`'
        plaintext: true
      - label: Removes the focused chip and moves focus to the next one, or the previous one when there is none, or the row itself when nothing is left.
        plaintext: true
---
::

Disabled chips are skipped throughout, and the ends wrap while `loop` is on and
stop while it is off. The remove buttons are never reached by Tab inside a
group, so a pointer user clicks them and a keyboard user presses Backspace.
