Skip to content

ExpressiveCSS

Browse documentation

Chips

Small blocks for contacts, tags, and filters.

View as Markdown

Add .chip to an element to make a chip. The element you pick sets which kind it is. The four Material 3 chip types and a non-interactive display chip use three root elements. Add outlined for a bordered style. Static chips are CSS only. The JavaScript plugin attaches to a .chips wrapper.

A long label wraps and the chip grows from its minimum height, with 4px above and below the lines. It never grows wider than its parent.

The label uses the label-large font-size token. The minimum height, icons, avatars, close buttons and editable input use rem sizes, so they grow with the browser's font size. At a 16px root, the label is 14px and the minimum height is 32px; at a 32px root, they are 28px and 64px. Padding, gaps and borders keep their px sizes.

Set --md-comp-chip-padding for left and right padding, --md-comp-chip-label-size for the label font size, and --md-comp-chip-icon-size for direct child icons. Each new name takes priority over its old counterpart, respectively --padding, --font-size and --font-size-icon. The old names are deprecated but still work when set on a chip, including fixed px values. There is no removal date. These properties leave block padding, avatars, close buttons and the editable input's sizes unchanged.

Custom sizing
<span class="chip" style="--md-comp-chip-padding: 12px; --md-comp-chip-label-size: 1rem; --md-comp-chip-icon-size: 1.25rem;">
  <span class="material-symbols" aria-hidden="true">info</span>
  Custom sizing
</span>
TypeElementWhy
Display <span class="chip"> Presents information. It is not a control and is not in the tab order.
Assist, suggestion <button type="button" class="chip"> One action on press.
Filter <input type="checkbox" class="chip-input"> + <label class="chip"> Multi-select and toggleable, and it carries a value into the form without JavaScript.
Input <span class="chip"> + a nested <button class="close"> The chip is a token. The delete button is the control.
Information Jane Doe Tag
<span class="chip outlined">Information</span>

<span class="chip">
  <img src="photo.jpg" alt=""> Jane Doe
</span>

<button type="button" class="chip">
  <span class="material-symbols" aria-hidden="true">event</span>
  Add to calendar
</button>

<input type="checkbox" class="chip-input" id="filter-flights">
<label class="chip" for="filter-flights">
  <span class="material-symbols" aria-hidden="true">check</span>
  Flights
</label>

<span class="chip">
  Tag
  <button type="button" class="close" aria-label="Remove Tag">
    <span class="material-symbols" aria-hidden="true">close</span>
  </button>
</span>

The delete button needs its own aria-label naming the chip it removes, because its only content is an icon. Mark the icon aria-hidden in every chip. The ligature is real text, and screen readers would otherwise read it out alongside the label.

A filter chip's selected state is :checked on its input, so it needs no script. Everywhere else, add the selected class for the selected look.

Clicking .close removes the chip only when it sits inside a .chips container. Importing the bundle runs Chips.Init() on DOMContentLoaded, which wires that click. A lone .chip does not remove itself.

Before 0.8.0 every chip was a <div class="chip"> and the delete control was an <i class="close">, focusable through tabindex but with no role or name. In 0.16.0 that <i> is no longer an icon and shows the word "close". Use the <button class="close"> shown here instead.

Contacts

Put an image inside the chip. The name next to it is the accessible name already, so the image is decorative and takes alt="".

Jane Doe
<span class="chip">
  <img src="photo.jpg" alt="">
  Jane Doe
</span>

Tags

Put a button.close inside the chip. Give it type="button" so it cannot submit a surrounding form, and an aria-label naming what it removes.

Tag
<span class="chip">
  Tag
  <button type="button" class="close" aria-label="Remove Tag">
    <span class="material-symbols" aria-hidden="true">close</span>
  </button>
</span>

JavaScript plugin

The plugin turns a .chips container into an editable tag field. Type a value and press Enter to add a chip. Delete with the chip's delete button, or select a chip and press Backspace or Delete. Selecting a chip marks it selected and moves focus to its delete button.

Backspace and Delete remove the focused chip and select the remaining chip before it, or else the first remaining chip or the input. Activating a delete button returns focus to the input. Deleting a chip from code does not move focus that is elsewhere. A deletion callback can move focus without the plugin resetting it. Long labels wrap while the delete control keeps its size.

allowUserInput defaults to false. Without it there is no text field and rendered chips have no delete button. Pass allowUserInput: true for the interactive field. AutoInit() starts every .chips except those with no-autoinit. It uses the defaults, so those wrappers stay display-only until you call init with options.

In a form, give the field's <input> a name. The chips submit under it, one id per chip, as a multiple select does. init wraps the input in an <expressive-chips-control>, a form-associated custom element with display: contents. It moves the name onto that element, so half-typed text is not submitted. The form treats the control as the field.

A disabled fieldset or a form reset applies to the control at once, including in a detached form or a shadow root. A canceled reset changes nothing. Setting disabled, readonly or form on the input applies at the next microtask.

A required input makes the field valid once it has a chip. Until then the field reports i18n.required, unless the input is read-only. The control holds that validity apart from any error the page sets on the input, and a blocked submit focuses the input. The input itself stays valid and does not match :invalid, so style or report the error from the control or the form. The input keeps aria-required="true".

destroy() unwraps the input and gives its name and required back. Without form-associated custom elements, as in jsdom, the plugin leaves the input as it is and the input submits its own text.

Pasting text that lists values, split by commas, tabs or new lines, adds a chip for each, up to limit. An autocompleteOnly field pastes as text. With allowCreate: true in autocompleteOptions, choosing the "Add" row adds the typed text as a chip, in an autocompleteOnly field too. The plugin then calls the page's own onCreate.

<form>
  <div class="chips">
    <input name="tags">
  </div>
</form>
<!-- Submits tags=red&tags=blue -->

An empty field. Type a tag and press Enter:

Initial tags from the data option:

Placeholders when the field is empty and after the first tag:

Autocomplete suggestions while typing:

<div class="chips"></div>
<div class="chips chips-initial"></div>
<div class="chips chips-placeholder"></div>
<div class="chips chips-autocomplete"></div>
<!-- Optional: provide your own input -->
<div class="chips">
  <input class="custom-class">
</div>

The classes chips-initial, chips-placeholder, and chips-autocomplete are only hooks for your selectors. They have no styles of their own.

Initialization

The IIFE bundle exposes Expressive.Chips. Call init with allowUserInput: true, plus any other options, for an editable field. Re-init after you add a container dynamically. Autocomplete keeps the query while filtering and clears it after the user selects an item.

document.addEventListener('DOMContentLoaded', function() {
  const elems = document.querySelectorAll('.chips');
  const instances = Expressive.Chips.init(elems, {
    allowUserInput: true,
    placeholder: 'Enter a tag',
    secondaryPlaceholder: '+Tag',
    autocompleteOptions: {
      data: [
        { id: 12, text: 'Apple' },
        { id: 13, text: 'Microsoft' },
        { id: 42, text: 'Google', image: 'https://picsum.photos/id/64/250/250' }
      ]
    }
  });
});

A chip data object looks like this. id is required, and the plugin does not render a chip without one.

const chip = {
  id: '4711',
  text: 'Title',
  image: ''
};

Options

Name Type Default Description
data Array [] Initial chips. Each item is a chip data object.
placeholder String '' Placeholder when there are no chips. Requires allowUserInput.
secondaryPlaceholder String '' Placeholder after at least one chip exists.
closeIconClass String 'material-symbols' Class on the icon inside the delete button.
allowUserInput Boolean false If true, render a text field and a delete button per chip, so the user can add and remove chips.
i18n Object { remove: 'Remove', required: 'Add at least one item.' } Strings the component generates. remove prefixes the delete button's accessible name, giving "Remove Apple". required is what a required field says while it has no chips.
autocompleteOptions Object {} Options passed to Autocomplete on the input. A non-empty object enables autocomplete.
autocompleteOnly Boolean false If true, Enter will not add a value that is not in the autocomplete list. An "Add" row from allowCreate still adds its text.
limit Number Infinity Maximum number of chips.
onChipAdd Function null Called after a chip is added. Receives the container and the chip element.
onChipSelect Function null Called when a chip is selected. Receives the container and the chip element.
onChipDelete Function null Called after a chip is deleted. Receives the container and the chip element.

Methods

All methods are called on the plugin instance. You can get the instance like this:
const instance = Expressive.Chips.getInstance(elem);
.addChip();

Add a chip, including when allowUserInput is false. Numeric id 0 is valid. Ignored if id is missing, already present, or the limit is reached.

instance.addChip({
  id: 1337,
  text: 'John Doe',
  image: ''
});
.deleteChip();

Delete the chip at this index.

instance.deleteChip(3);
.selectChip();

Focus the chip at this index.

instance.selectChip(2);
.getData();

Return the current chips as an array of chip data objects.

instance.getData();
.destroy();

Destroy the plugin instance, remove rendered chips, and tear down its event handlers. This also destroys the owned Autocomplete and its Menu.

instance.destroy();

Properties

Name Type Description
el Element The container the plugin was initialized with.
options Object The options the instance was initialized with.
chipsData Array The current chip data.
hasAutocomplete Boolean Whether autocomplete is enabled.
autocomplete Autocomplete The Autocomplete instance, if any.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow