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.
<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>
| Type | Element | Why |
|---|---|---|
| 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. |
<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="".
<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.
<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. |