# Chip Glide

> Moves a wrapping row of chips when values come and go. A removed chip fades where it sits, then the others glide to their new places, and one that changes lines fades across so it never passes over the rest. It shows nothing.

- Name: `chip-glide`
- Status: beta
- Categories: motion, forms
- File: the install writes `chip-glide.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`. The install adds them.
- Other registry items: `@arbokui/motion-timing`. The install adds them.
- Gallery: https://arbokui.com/components/chip-glide
- Registry item: https://arbokui.com/r/chip-glide.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/chip-glide
   ```

## Use it for

- A row of chips that people add to and remove from, like Token Field and Combobox with multiple.
- Make a leaving chip inert, so a press passes through it. Give the box content-start, so its lines stay put while it glides to a new height.
- Spread box and render rows to glide the chips. Add hold and tip to keep the pressed remove button in place and give it its tooltip.

## Avoid it for

- A list that changes all at once, like search results. Fade the new list in.
- Rows that reorder by drag. Use a reorder list.

## Props

- `useChipGlide`: `(values: readonly string[]) => { rows, box, hold, tip }`. Hook. Pass the values in order. Returns the rows to render, the props for the wrapping box, and hold and tip for each remove button.
- `rows`: `{ value: string; leaving: boolean }[]`. What to render, in order. A removed value stays, leaving, until it has faded.
- `box`: `{ ref, onPointerLeave, onKeyDown }`. Spread on the element that wraps the chips and the text field. Put data-glide on each chip, set to its value, data-glide="" on the text field, and data-spot on each remove button.
- `hold`: `(value: string) => (event: PointerEvent) => void`. The remove button's onPointerDown. A mouse holds each chip it removes in place, faded, until it leaves the box or a key is pressed in it, so the next chip's remove button stays under it. A finger does not.
- `tip`: `(value: string) => { handle, id }`. The remove button's tooltip. Spread it on the TooltipTrigger, and render one Tooltip per row, outside the chips, with this handle. It fades out where its chip was.

## Accessibility

- A leaving chip is inert and hidden from screen readers while it fades, so it is never read twice or pressed.
- A press where a held chip was lands on the box behind it, not on the next chip or the text field.
- A chip removed from the keyboard hands focus to the next chip's remove button, or the one before it.
- Reduced motion keeps the fades, and puts each chip in its new place at once. Motion off does both at once.

## Examples

### Remove chips and glide the rest (needs @arbokui/tooltip)

```tsx
function Labels({ values, onRemove }: { values: string[]; onRemove: (value: string) => void }) {
  const glide = useChipGlide(values);
  return (
    <div {...glide.box} className="flex flex-wrap content-start gap-1">
      {glide.rows.map(({ value, leaving }) => (
        <span key={value} data-glide={value} inert={leaving} className="inline-flex items-center gap-1 rounded-md bg-secondary ps-2">
          {value}
          <TooltipTrigger {...glide.tip(value)} data-spot aria-label={`Remove ${value}`} onPointerDown={glide.hold(value)} onClick={() => onRemove(value)}>
            ×
          </TooltipTrigger>
        </span>
      ))}
      {glide.rows.map(({ value }) => (
        <Tooltip key={value} handle={glide.tip(value).handle}>
          <TooltipPopup>Remove {value}</TooltipPopup>
        </Tooltip>
      ))}
    </div>
  );
}
```
