# Chip
Represent a selected or applied value that can be removed.
Section: components
Docs: https://nebula.kaluza.com/components/chip
---
## When to use
- To display a selected value from a multi-select input, such as a chosen filter or a tag applied by the user, alongside a way to remove it
- To show applied filters in a filter row where each item can be dismissed individually
## When not to use
- **For read-only labels or status tags** - use [Badge](/components/badge) instead. Chip always includes a remove button; if the value cannot be removed, it is not a Chip.
- **For selectable options in a form** - use [Checkbox](/components/checkbox) for multi-select or [Radio](/components/radio) for single-select. Chip is for representing values that have already been chosen, not for making the choice.
- **For primary actions or navigation** - use [CTA Button](/components/cta-button) or [Text Link](/components/text-link) instead.
## Anatomy
1. **Label**
2. **Remove button**
3. **Tooltip** (appears automatically when the label is truncated, or when a custom tooltip is provided)
## Basic usage
```tsx
Removable chip
```
## Variants and features
### Removable
Every Chip includes a remove button. The `onRemove` callback fires when the button is clicked or when the user presses Enter or Space on it.
```tsx
const Example = () => {
const [filters, setFilters] = useState(['Electricity', 'Gas', 'EV owner']);
return (
{filters.map(filter => (
setFilters(filters.filter(f => f !== filter))}>
{filter}
))}
);
}
```
### Truncation and tooltip
Chip has a maximum width of `20rem`. Labels that exceed this width are truncated with an ellipsis. When truncation occurs, hovering or focusing the remove button shows the full label text in a tooltip automatically. No additional configuration is needed.
```tsx
This chip has a very long label so it will be truncated
```
If you want to show custom tooltip content regardless of truncation, use the `tooltip` prop. This is useful when the label is an acronym or abbreviated form.
```tsx
MSN
```
## Content
Keep chip labels short, ideally one to three words. Write them in sentence case. If using an acronym, provide the expanded form in the `tooltip` prop so screen reader users and users unfamiliar with the acronym can understand the full meaning.
## Rules and guidance
### Usage
- **Chip** MUST only be used when the represented value can be removed by the user. Rationale: Chip always renders a remove button. If the value is read-only and cannot be removed, the button is a misleading affordance. Use Badge for non-removable labels. [id: chip.removable-only; testability: context]
- **Chip** MUST NOT be used to trigger primary page actions or navigate between pages. Rationale: Chip's visual design and accessible role communicate "this is a removable value", not "this is a primary action". Using it for actions creates a mismatch between appearance and behaviour. [id: chip.not-for-actions; testability: manual]
### Content
- If the label is an acronym or abbreviation: **Chip tooltip** MUST contain the full expanded form of the acronym when the label is an acronym or abbreviation. Rationale: Screen reader users hear the remove button's accessible name, which defaults to the label text. An unexpanded acronym is meaningless as a standalone accessible name. The tooltip content becomes part of the screen reader announcement. [id: chip.acronym-tooltip; testability: manual]
- **Chip label** SHOULD be written in sentence case. [id: chip.label-sentence-case; testability: manual]
- **Chip label** SHOULD be one to three words. Rationale: Chips are compact by design. Long labels cause truncation, which hides information and relies on the tooltip to convey meaning. If the value requires more than three words to be understood, consider displaying it differently. [id: chip.label-concise; testability: manual]
### Interaction
- **Chip** WILL show a tooltip with the full label text when the label is truncated due to the 20rem max-width constraint. Rationale: The component detects truncation via scrollWidth comparison and shows a tooltip automatically. No consumer implementation is needed. [id: chip.will-truncation-tooltip; testability: auto]
- **Chip** SHOULD have an onRemove callback that removes or deselects the value it represents. Rationale: The remove button is always rendered. Without an onRemove handler, clicking it does nothing, which is a broken affordance. The only valid case for omitting onRemove is when the Chip is temporarily non-interactive and this state is communicated visually. [id: chip.onremove-wired; testability: manual]
### Composition
- **Chip ref** MUST be used with the understanding that the forwarded ref points to the remove button element, not the outer chip span. Rationale: Chip forwards its ref to the StyledButton (the remove button). Code that relies on the ref to access the outer chip container will get the wrong element. [id: chip.ref-targets-button; testability: auto]
## Accessibility
### In brief
Chip is a text label with a native remove button. A screen reader reads the label as text, then a button named with the label and "press to remove".
### What Nebula provides
- The remove button is a native `button` and the chip's only tab stop. The cross icon is hidden from screen readers.
- The button is named with the label, or the `tooltip` when one is set, followed by the hidden text "press to remove".
- A focus ring around the whole chip when the remove button has keyboard focus.
- A tooltip shows the `tooltip` text, or the full label if it is cut off, while the remove button has focus or the chip is hovered.
### Keyboard behaviour
The chip is reached through its remove button with `Tab`, and removed with `Enter` or `Space`.
#### Interactions
- **Tab** - Moves focus to the remove button
- **Enter or Space** - Calls `onRemove`
### Accessible design
#### If you set a `tooltip`, write it so it makes sense without the label
The `tooltip` text replaces the label in the remove button's name, so a screen reader user hears only the tooltip when they reach the button. "Meter Serial Number" works for a chip labelled "MSN"; "Billing" does not for a chip labelled "General enquiry".
WCAG: [2.4.6 Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html)
### Accessible development
#### After a chip is removed, move focus somewhere sensible
The remove button leaves the page with its chip, so focus is lost. Move it to the next chip, or to the control that added the chips when none are left. The chip's `ref` is its remove button.
WCAG: [2.4.3 Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html)
#### Put what a user needs in the label, not only in the `tooltip`
A tooltip only appears on hover or on keyboard focus of the remove button, so a user on a touch screen is unlikely to see it.
WCAG: [1.4.13 Content on Hover or Focus](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html)
### Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
| --- | --- | --- |
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Partially tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What each test covers:
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
What these statuses mean:
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
## Properties
### Chip
- `children` (type `ReactNode`, required). Label shown inside the chip. Truncated with an ellipsis beyond 20rem, in which case it is shown in a tooltip on hover or focus.
- `tooltip` (type `ReactNode`, optional). Content for a tooltip shown when the chip is hovered or its remove button is focused. Replaces the automatic tooltip for truncated labels, and is read out as the remove button's label.
- `onRemove` (type `() => void`, optional). Called when the remove button is clicked or activated by keyboard.
- `onKeyDown` (type `(e: KeyboardEvent) => void`, optional). Called on key presses while the remove button has focus, for example to implement arrow key navigation between chips.
- `tabIndex` (type `number`, optional). Tab index of the remove button, which is the chip's only focusable element. Use `-1` to manage focus yourself, for example in a roving tabindex group.
## Styles
Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS.
### span
```css
display: inline-flex;
align-items: center;
height: ${space[6]};
line-height: 1.9rem;
max-width: 20rem;
overflow: hidden;
border-radius: ${radii.standard};
border: ${borderWidths.standard} solid ${grey[400]};
background: ${grey[200]};
padding-top: ${space[1]};
padding-bottom: ${space[1]};
padding-left: ${space[2]};
gap: ${space[1]};
color: ${colors.body};
&:has(button:focus-visible) {
${focusRing}
}
```
### text
```css
font-family: ${fonts.body};
font-weight: ${fontWeights.medium};
font-size: ${responsiveFontSizes.body[1]};
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
```
### button
```css
border: none;
display: flex;
justify-content: center;
align-items: center;
border-radius: 0 ${radii.standard} ${radii.standard} 0;
width: ${space[6]};
height: ${space[6]};
background: ${grey[200]};
&:hover {
background: ${grey[300]};
cursor: pointer;
}
&:focus-visible {
background: ${grey[300]};
outline: none;
}
& > svg {
color: ${colors.body};
flex-shrink: 0;
width: ${space[4]};
height: ${space[4]};
}
```