Skip to content
Components, switch product

Tags

<gui-tags> turns what the user types into a list of tags. Each tag shows as a pill with a remove button, before a text input for the next one. Type a skill below and press Enter.

Code of the example above
TagsBasic.tsx
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TagsBasic() {
return <GuiTags label="Skills" placeholder="Type a skill and press Enter" />;
}

The element’s entry point is @golemui/gui-components/tags, which registers <gui-tags>, and <gui-pills>, which draws its tags. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

SkillsField.tsx
import { useState } from 'react';
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function SkillsField() {
const [skills, setSkills] = useState([] as string[]);
return (
<GuiTags
label="Skills"
name="skills"
value={skills}
onGuiChange={(event) => setSkills(event.detail.value ?? [] as string[])}
/>
);
}

Properties

Property Type Default Description
localeId attribute locale-id string 'en' BCP 47 locale for formatting and parsing, such as en-US or es.
value string[] [] The tags.
placeholder string – Text shown while the control is empty.
icon string – Icon class name shown inside the control, for example from an icon font.
separators TagsSeparator[] – Keys that turn the typed text into a tag: Enter, Tab or a character such as , or ;, which also splits pasted text. blur adds it when focus leaves.
allowDuplicates attribute allow-duplicates boolean true Allows the same tag more than once. On by default: allow-duplicates="false" turns it off.
trim boolean true Removes spaces around each tag. On by default: trim="false" turns it off.
removeAriaLabel attribute remove-aria-label string – Accessible name of the remove button of each tag. An empty value keeps the default.
removeIcon attribute remove-icon string – Icon class name of the remove button, replacing the default ×.
uid string – The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration.

Form control properties

Every field has these, from the base class of the form controls.

Property Type Default Description
name string – The name the value is submitted under. Without it the element stays out of its form. Reflected, like a native control's: the form reads a single value's name from the attribute.
label string – The visible label, which also names the control for assistive technology. Without one the control has no accessible name; the date, time, range, tags and file fields warn about it in development builds.
hint string – Help text shown under the label and announced as the control's description.
errors string[] [] Error messages to show, typically from your own validation. They show as soon as they are set, unless touched is false.
touched boolean – Whether the user has interacted with the control. Leave it unset unless you validate on interaction: false holds errors back until it becomes true.
required boolean false The control needs a value: it is marked as required and, with a name, blocks submission.
readOnly attribute readonly boolean – The value can be read and focused but not changed.
disabled boolean – Disabled by its own attribute or by a disabled <fieldset> around it. Setting the property sets the attribute, like a native control's.
form read-only, no attribute HTMLFormElement | null – The <form> the element belongs to, if any.
validity read-only, no attribute ValidityState – The element's validity, like a native control's.
validationMessage read-only, no attribute string – The message the browser shows for an invalid value.

Methods

Method Description
checkValidity(): boolean –
reportValidity(): boolean –

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-input The user changed the value. detail.value is the new value.
gui-change The user committed the value. detail.value is the committed value.
gui-blur Focus left the control.

CSS custom properties

Property Default Description
--gui-pill-height – Height of each pill.
--gui-pill-font-size – Font size of the pill text.
--gui-pill-action-size – Size of the icons inside a pill.
--gui-pill-action-hit – Clickable area of the buttons inside a pill.

The value is the list of tags, an array of strings. As an attribute it is a JSON array.

The user adds a tag by typing it and pressing Enter, typing a comma, pressing Tab, or leaving the field: each of these commits the text typed so far. Pasted text that holds commas or line breaks becomes several tags at once. Escape clears the text without adding it.

To remove a tag, the user clicks its remove button, or presses Backspace in the empty input to reach the last tag, then Delete or Backspace. Every change fires gui-input and gui-change with the new list.

Code of the example above
TagsValue.tsx
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const keywords = ['Lisbon', 'Weekend', 'Food tour'];
export function TagsValue() {
return (
<GuiTags
label="Keywords"
hint="Press Backspace in the empty field to reach the tags."
value={keywords}
/>
);
}

separators sets what commits a tag: Enter, Tab, blur (leaving the field) or any single character, such as ;. A character separator is never typed into the field, and splits pasted text too. The default is ["Enter", ",", "Tab", "blur"]. Here a semicolon separates the addresses, so a comma can’t commit one by accident:

Code of the example above
TagsSeparators.tsx
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const separators = [';', 'Enter', 'blur'];
export function TagsSeparators() {
return (
<GuiTags
label="Recipients"
placeholder="ada@example.com; grace@example.com"
separators={separators}
/>
);
}

The field accepts the same tag more than once by default. allow-duplicates="false" drops a tag that is already in the list, and clears what the user typed. The field also trims the spaces around each tag; trim="false" keeps them. Try adding bug again:

Code of the example above
TagsDuplicates.tsx
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const labels = ['bug', 'help wanted'];
export function TagsDuplicates() {
return <GuiTags label="Labels" allowDuplicates={false} value={labels} />;
}

icon takes the class names of an icon and shows it at the start of the field, before the tags. The icon is decorative and hidden from assistive technology. remove-icon replaces the remove button’s × the same way. See Icons for the ways to draw them.

Code of the example above
TagsIcon.tsx
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const topics = ['Design', 'Research'];
// Any icon font works: icon sets these classes on the icon element.
// This one draws a Material Icons ligature.
const iconStyles = `
.icon-tag::before {
content: 'sell';
font-family: 'Material Icons';
}
`;
export function TagsIcon() {
return (
<>
<GuiTags label="Topics" icon="icon-tag" value={topics} />
<style>{iconStyles}</style>
</>
);
}

When the field is narrower than 540px, the tags collapse into a count. The count is a button that opens the tags in a list below the field, where the user can remove them. Each field below is narrow enough:

Code of the example above
TagsNarrow.tsx
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TagsNarrow() {
return (
<div className="gui-grid gui-grid--row">
<div className="gui-grid__cell">
<GuiTags label="Allergies" value={['Peanuts', 'Shellfish', 'Gluten']} />
</div>
<div className="gui-grid__cell">
<GuiTags label="Diets" value={['Vegetarian', 'Halal']} />
</div>
</div>
);
}

A disabled field can’t be focused or changed, and a form doesn’t submit it. A readonly field shows its tags without remove buttons, and the user can’t add any; a form submits its value.

Code of the example above
TagsDisabledReadonly.tsx
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TagsDisabledReadonly() {
return (
<div className="gui-grid gui-grid--row">
<div className="gui-grid__cell">
<GuiTags label="Plan features" value={['SSO']} disabled />
</div>
<div className="gui-grid__cell">
<GuiTags label="Roles" value={['Admin', 'Billing']} readOnly />
</div>
</div>
);
}

errors takes a list of messages from your own validation, and shows them under the field. To validate as the user works, set the errors property from a gui-change listener. Add a fourth interest:

Code of the example above
TagsValidation.tsx
import { useState } from 'react';
import { GuiTags } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TagsValidation() {
const [errors, setErrors] = useState<string[]>([]);
return (
<GuiTags
id="interests-tags"
label="Interests"
hint="Up to three."
errors={errors}
onGuiChange={(event) =>
setErrors(
(event.detail.value ?? []).length > 3
? ['Keep it to three interests.']
: [],
)
}
/>
);
}

With a name, <gui-tags> submits one entry per tag, all under that name, and nothing while the list is empty. Read them with FormData.getAll(). required blocks the submit until there is at least one tag. Text typed but not committed yet isn’t part of the value. Submit the form below, then add a couple of tags and submit again.

Code of the example above
TagsNativeForm.tsx
import { useState, type SubmitEvent } from 'react';
import {
GuiButton,
GuiTags,
GuiTextinput,
} from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TagsNativeForm() {
const [title, setTitle] = useState<string | undefined>(
'Notes on the Analytical Engine',
);
const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
// getAll: the tags field submits one entry per tag.
const post = { title: data.get('title'), tags: data.getAll('tags') };
setResult(`Submitted ${JSON.stringify(post)}`);
};
return (
<form id="post-form" onSubmit={onSubmit}>
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiTextinput
label="Title"
name="title"
value={title}
onGuiInput={(event) => setTitle(event.detail.value)}
/>
</div>
<div className="gui-grid__cell">
<GuiTags label="Tags" name="tags" required />
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Publish" />
</div>
</div>
<p id="post-result" role="status">
{result}
</p>
</form>
);
}

How the elements submit, validate and reset is in Native forms.

<gui-pills> is the strip of pills gui-tags draws its tags with: each pill is a button, with a remove icon, and the strip collapses into a count with a dropdown when it doesn’t fit. It shows the items you give it, { key, label } objects, and fires gui-pill-remove with the pill’s key when the user removes one: the host keeps the list and removes the item. Use it alone to show a list of chosen items in a field of your own. For it to collapse into a count, wrap it in an element with container-type: inline-size and container-name: gui-pills-widget, and position: relative for its dropdown.

Properties

Property Type Default Description
items GuiPillItem[] [] The pills.
clickable boolean false Makes the pill bodies clickable, firing gui-pill-click.
removable boolean true Adds a remove button to each pill, unless the pills are disabled or read-only.
bubble boolean true Collapses the pills into a count with a dropdown when they do not fit.
tabbable boolean true When false, pills are not in the tab cycle (entered via ArrowLeft instead).
disabled boolean false Disables the pills.
readOnly attribute readonly boolean false Shows the pills without remove buttons or editing.
editable boolean false When true, clickable pills expose selection as toggle-button semantics (aria-pressed).
selectedKey attribute selected-key string – Key of the pill selected by the host's allowEdit flow.
editingKey attribute editing-key string – Key of the pill with an in-flight edit session (its label live-previews the working value).
editLabel attribute edit-label string – Tooltip labels for the editable action icons rendered inside the pills.
confirmEditLabel attribute confirm-edit-label string – Tooltip of the confirm button of the pill being edited.
cancelEditLabel attribute cancel-edit-label string – Tooltip of the cancel button of the pill being edited.
removeAriaLabel attribute remove-aria-label string – Accessible name of each remove button.
removeIcon attribute remove-icon string – Icon class name of the remove button, replacing the default ×.
compactAriaLabel attribute compact-aria-label string – Accessible name of the count shown when the items do not fit. {count} is the number of items. An empty value keeps the default.
toolbarAriaLabel attribute toolbar-aria-label string – Accessible name of the pill strip. An empty value removes it.
errors string[] – Errors repeated inside the dropdown.
touched boolean – Whether the field was touched: errors wait for it unless unset.
uid string – The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration.

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-pill-click A pill body was clicked (clickable only). detail.key is the pill's key.
gui-pill-remove The × was pressed, or Delete/Backspace on a focused pill. detail.key is the pill's key.
gui-dropdown-toggle The count bubble opened or closed its dropdown. detail.open is the new state.
gui-pill-keydown A key the strip does not handle, or one at its boundaries, so the host can intercept it (for example to focus its own input on ArrowRight past the end). detail has the pill key and the keyboard event.
gui-pill-exit Focus left the pills involuntarily (Escape closed the dropdown); the host should restore focus to its anchor element. detail has the key and the reason.
gui-pill-focus A pill received focus (editable hosts only). detail.key is its key.
gui-pills-blur Focus left the pills (editable hosts only).
gui-pill-edit The edit icon was pressed, or F2 or E on a focused pill (editable hosts only). detail.key is the pill's key.
gui-pill-edit-confirm The confirm icon was pressed on the pill being edited. detail.key is its key.
gui-pill-edit-cancel The cancel icon was pressed on the pill being edited. detail.key is its key.

CSS custom properties

Property Default Description
--gui-pill-height – Height of each pill.
--gui-pill-font-size – Font size of the pill text.
--gui-pill-action-size – Size of the icons inside a pill.
--gui-pill-action-hit – Clickable area of the buttons inside a pill.

<gui-tags> renders into the light DOM, so you can style every part with plain CSS. These are the parts and their public classes:

gui-tags
<gui-tags class="gui-field">
<span class="gui-label" id="skills_label">
<span class="gui-label__text">
Skills<span aria-hidden="true"> *</span>
</span>
<div class="gui-widget-hint" id="skills_hint" aria-hidden="true">
Press Enter after each one.
</div>
</span>
<div class="gui-widget">
<div
role="group"
class="gui-widget-input gui-tags-input"
aria-labelledby="skills_label"
aria-describedby="skills_hint"
aria-invalid="true"
aria-errormessage="skills_errors"
>
<gui-pills class="gui-tags__pills">
<div class="gui-pills__strip-wrapper">
<div
class="gui-pills__strip"
role="toolbar"
tabindex="-1"
aria-label="Selected tags"
>
<span class="gui-sentinel gui-sentinel__start"></span>
<button
type="button"
class="gui-pills__pill"
tabindex="-1"
aria-label="TypeScript"
aria-description="Remove tag"
>
<span class="gui-pills__pill-text">TypeScript</span>
<span class="gui-pills__pill-remove" aria-hidden="true"
><svg>…</svg></span
>
</button>
<!-- One pill per tag. -->
<span class="gui-sentinel gui-sentinel__end"></span>
</div>
</div>
<div class="gui-pills__compact">
<button
type="button"
class="gui-pills__count"
aria-label="2 tags"
aria-haspopup="true"
aria-expanded="false"
aria-controls="skills_pills_dropdown"
>
2
</button>
</div>
</gui-pills>
<input
type="text"
class="gui-tags__input"
id="skills"
aria-labelledby="skills_label"
aria-required="true"
placeholder="Type a skill and press Enter"
/>
</div>
</div>
<div class="gui-validator" id="skills_errors" role="alert">
<div class="gui-validator__error">Add at least one skill.</div>
</div>
</gui-tags>
ClassPart
gui-fieldThe element itself. Lines its parts up with the fields next to it in a gui-grid row.
gui-labelThe label, with the text and the hint.
gui-label__textThe label text and the required marker.
gui-widget-hintThe hint.
gui-widgetThe box around the field.
gui-widget-inputThe part that looks like a field: the tags and the input.
gui-tags-inputThe same part: the container the pills measure to collapse into a count.
gui-tags-input--iconOn that part when there is an icon.
gui-widget-iconThe icon, with your icon classes.
gui-tags__pillsThe gui-pills element.
gui-pills__strip-wrapperThe box of the strip, hidden while the tags show as a count.
gui-pills--start-shadow, gui-pills--end-shadowOn the box while tags are scrolled out of view at either end: they fade that end.
gui-pills__stripThe row of pills, which scrolls when they don’t fit.
gui-pills__pillA tag.
gui-pills__pill-textThe text of a tag.
gui-pills__pill-removeThe remove button of a tag.
gui-pills__compactThe box of the count, shown while the field is narrow.
gui-pills__countThe count of tags.
gui-pills__dropdownThe list of tags the count opens.
gui-pills__dropdown-listThe tags in that list.
gui-tags__inputThe text input.
gui-validatorThe errors container. It is always there, so the alert role announces new errors.
gui-validator__errorOne error message.

The ids derive from uid, or from a generated id such as gui-tags-3 when you don’t set one. Select by class or by tag, not by id.

The field reads these design tokens. Set them on :root to change every tags field, or on any element to change the ones inside it:

TokenWhat it styles
--gui-widget-sizeThe least height of the field, and the size of the count’s box.
--gui-radius-mdThe corner radius of the field, the pills and the tags list.
--gui-bg-surfaceThe field’s background.
--gui-border-defaultThe field’s border, the line after the pills, and the tags list’s border.
--gui-text-defaultThe typed text and the label.
--gui-font-familyThe font.
--gui-font-smThe text size of the label and the field.
--gui-font-xsThe text size of the hint, the errors and the count.
--gui-text-secondaryThe hint.
--gui-intent-primaryThe pills’ and the count’s background.
--gui-color-whiteThe pills’ and the count’s text.
--gui-pill-heightThe height of each pill.
--gui-pill-font-sizeThe text size of the pills.
--gui-pill-action-sizeThe size of the remove icon.
--gui-pill-action-hitThe clickable area of the remove button.
--gui-space-2The space between the pills, and around the input.
--gui-bg-defaultThe tags list’s background, and the fade at the strip’s ends.
--gui-shadow-mdThe tags list’s shadow.
--gui-border-focusThe field’s border while the input or a pill has focus.
--gui-shadow-focusThe focus ring, around the field and the focused pill.
--gui-intent-errorThe field’s border and the messages when there are errors.
--gui-shadow-focus-errorThe focus ring when there are errors.
--gui-bg-disabledThe background when disabled.
--gui-text-disabledThe text when disabled.
--gui-space-1The space between the label, hint and errors.
styles.css
/* Taller pills with larger text, in every tags field. */
:root {
--gui-pill-height: 28px;
--gui-pill-font-size: var(--gui-font-sm);
}

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Name. The label names the input and the group around the tags, through aria-labelledby. Without a label the field has no accessible name, and development builds log a warning.
  • Description. The hint is the description of the group, through aria-describedby.
  • Tags. The tags sit in a toolbar named “Selected tags”. Each tag is a button named by its text, described as “Remove tag”; remove-aria-label replaces that description. The remove icon is hidden from screen readers: Delete does the same.
  • Count. In a narrow field, the count is a button named after the number of tags, such as “2 tags”. A click, Enter, Space or ↓ opens the list of tags.
  • Errors. While errors show, the group has aria-invalid="true" and points at them with aria-errormessage, and screen readers announce new errors as they appear.
  • Required and read-only. required sets aria-required on the input; the visual marker is hidden from screen readers.
  • Tab order. The tags aren’t in the tab order: Tab lands on the input, and the keys below reach the tags. In a narrow field, the count comes first.

In the input:

KeyAction
Enter ,Adds the typed text as a tag, with the default separators.
TabAdds the typed text as a tag, and moves focus on.
EscapeClears the typed text.
BackspaceIn the empty input, moves focus to the last tag.
←With the caret at the start, moves focus to the last tag.
↓In a narrow field, opens the list of tags, on the first one.

On a tag:

KeyAction
← →Moves to the previous or next tag. Past the last one, back to the input.
↑ ↓In the list of a narrow field, moves to the previous or next tag.
Home EndMoves to the first or last tag.
Delete BackspaceRemoves the tag, and fires gui-change.
EscapeIn the list of a narrow field, closes it and goes back to the input.

What every element shares, such as translating the browser messages, is in Accessibility.