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.
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" />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/tags';
@Component({ selector: 'app-tags-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-tags label="Skills" placeholder="Type a skill and press Enter" ></gui-tags> `,})export class TagsBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';</script>
<template> <gui-tags label="Skills" placeholder="Type a skill and press Enter" ></gui-tags></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
@customElement('tags-basic')export class TagsBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-tags label="Skills" placeholder="Type a skill and press Enter" ></gui-tags> `; }}<gui-tags label="Skills" placeholder="Type a skill and press Enter"></gui-tags>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.
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[])} /> );}Add CUSTOM_ELEMENTS_SCHEMA to the component that uses the tags, and the stylesheet to styles in angular.json.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiEventMap, GuiTagsEvents } from '@golemui/gui-components';import '@golemui/gui-components/tags';
type ChangeEvent = GuiEventMap<typeof GuiTagsEvents>['gui-change'];
@Component({ selector: 'app-skills-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-tags label="Skills" name="skills" [value]="skills" (gui-change)="onChange($event)" ></gui-tags> `,})export class SkillsFieldComponent { skills = [] as string[];
onChange(event: Event) { this.skills = (event as ChangeEvent).detail.value ?? [] as string[]; }}"styles": ["node_modules/@golemui/gui-components/lib/styles/index.css"]Tell Vue's compiler that gui-* tags are custom elements, and add the typings once for typed props and @gui-* handlers.
<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const skills = ref([] as string[]);</script>
<template> <gui-tags label="Skills" name="skills" :value="skills" @gui-change="skills = $event.detail.value ?? [] as string[]" /></template>vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') } },});import type {} from '@golemui/gui-components/vue';The page's stylesheet styles the gui-* elements, so it only reaches them outside a shadow root. Render the host component into the light DOM too, as below, or load the stylesheet into its shadow root.
import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
@customElement('skills-field')export class SkillsField extends LitElement { @state() private skills = [] as string[];
protected override createRenderRoot() { return this; }
override render() { return html` <gui-tags label="Skills" name="skills" .value=${this.skills} @gui-change=${(event: CustomEvent) => (this.skills = event.detail.value)} ></gui-tags> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-tags label="Skills" name="skills"></gui-tags>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-tags');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});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. |
Examples
Section titled “Examples”Adding and removing tags
Section titled “Adding and removing tags”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.
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/tags';
@Component({ selector: 'app-tags-value', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-tags label="Keywords" hint="Press Backspace in the empty field to reach the tags." [value]="keywords" ></gui-tags> `,})export class TagsValueComponent { keywords = ['Lisbon', 'Weekend', 'Food tour'];}<script setup lang="ts">import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const keywords = ['Lisbon', 'Weekend', 'Food tour'];</script>
<template> <gui-tags label="Keywords" hint="Press Backspace in the empty field to reach the tags." :value.prop="keywords" ></gui-tags></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const keywords = ['Lisbon', 'Weekend', 'Food tour'];
@customElement('tags-value')export class TagsValue extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-tags label="Keywords" hint="Press Backspace in the empty field to reach the tags." .value=${keywords} ></gui-tags> `; }}<gui-tags label="Keywords" hint="Press Backspace in the empty field to reach the tags." value='["Lisbon", "Weekend", "Food tour"]'></gui-tags>Separators
Section titled “Separators”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:
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/tags';
@Component({ selector: 'app-tags-separators', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-tags label="Recipients" placeholder="ada@example.com; grace@example.com" [separators]="separators" ></gui-tags> `,})export class TagsSeparatorsComponent { separators = [';', 'Enter', 'blur'];}<script setup lang="ts">import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';</script>
<template> <gui-tags label="Recipients" placeholder="ada@example.com; grace@example.com" :separators.prop="[';', 'Enter', 'blur']" ></gui-tags></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const separators = [';', 'Enter', 'blur'];
@customElement('tags-separators')export class TagsSeparators extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-tags label="Recipients" placeholder="ada@example.com; grace@example.com" .separators=${separators} ></gui-tags> `; }}<gui-tags label="Recipients" placeholder="ada@example.com; grace@example.com" separators='[";", "Enter", "blur"]'></gui-tags>Duplicates and spaces
Section titled “Duplicates and spaces”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:
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} />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/tags';
@Component({ selector: 'app-tags-duplicates', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-tags label="Labels" allow-duplicates="false" [value]="labels" ></gui-tags> `,})export class TagsDuplicatesComponent { labels = ['bug', 'help wanted'];}<script setup lang="ts">import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const labels = ['bug', 'help wanted'];</script>
<template> <gui-tags label="Labels" :allowDuplicates="false" :value.prop="labels" ></gui-tags></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const labels = ['bug', 'help wanted'];
@customElement('tags-duplicates')export class TagsDuplicates extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-tags label="Labels" allow-duplicates="false" .value=${labels} ></gui-tags> `; }}<gui-tags label="Labels" allow-duplicates="false" value='["bug", "help wanted"]'></gui-tags>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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation,} from '@angular/core';import '@golemui/gui-components/tags';
@Component({ selector: 'app-tags-icon', schemas: [CUSTOM_ELEMENTS_SCHEMA], encapsulation: ViewEncapsulation.None, template: ` <gui-tags label="Topics" icon="icon-tag" [value]="topics"></gui-tags> `, styles: ` /* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */ .icon-tag::before { content: 'sell'; font-family: 'Material Icons'; } `,})export class TagsIconComponent { topics = ['Design', 'Research'];}<script setup lang="ts">import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const topics = ['Design', 'Research'];</script>
<template> <gui-tags label="Topics" icon="icon-tag" :value.prop="topics"></gui-tags></template>
<style>/* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */.icon-tag::before { content: 'sell'; font-family: 'Material Icons';}</style>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const topics = ['Design', 'Research'];
@customElement('tags-icon')export class TagsIcon extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-tags label="Topics" icon="icon-tag" .value=${topics}></gui-tags>
<style> /* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */ .icon-tag::before { content: 'sell'; font-family: 'Material Icons'; } </style> `; }}<gui-tags label="Topics" icon="icon-tag" value='["Design", "Research"]'></gui-tags>
<style> /* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */ .icon-tag::before { content: 'sell'; font-family: 'Material Icons'; }</style>Narrow fields
Section titled “Narrow fields”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:
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/tags';
@Component({ selector: 'app-tags-narrow', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Allergies" [value]="allergies"></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Diets" [value]="diets"></gui-tags> </div> </div> `,})export class TagsNarrowComponent { allergies = ['Peanuts', 'Shellfish', 'Gluten']; diets = ['Vegetarian', 'Halal'];}<script setup lang="ts">import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const allergies = ['Peanuts', 'Shellfish', 'Gluten'];const diets = ['Vegetarian', 'Halal'];</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Allergies" :value.prop="allergies"></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Diets" :value.prop="diets"></gui-tags> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const allergies = ['Peanuts', 'Shellfish', 'Gluten'];const diets = ['Vegetarian', 'Halal'];
@customElement('tags-narrow')export class TagsNarrow extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Allergies" .value=${allergies}></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Diets" .value=${diets}></gui-tags> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Allergies" value='["Peanuts", "Shellfish", "Gluten"]'></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Diets" value='["Vegetarian", "Halal"]'></gui-tags> </div></div>Disabled and read-only
Section titled “Disabled and read-only”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/tags';
@Component({ selector: 'app-tags-disabled-readonly', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Plan features" [value]="features" disabled></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Roles" [value]="roles" readonly></gui-tags> </div> </div> `,})export class TagsDisabledReadonlyComponent { features = ['SSO']; roles = ['Admin', 'Billing'];}<script setup lang="ts">import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const features = ['SSO'];const roles = ['Admin', 'Billing'];</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Plan features" :value.prop="features" :disabled="true" ></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Roles" :value.prop="roles" :readOnly="true"></gui-tags> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const planFeatures = ['SSO'];const roles = ['Admin', 'Billing'];
@customElement('tags-disabled-readonly')export class TagsDisabledReadonly extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Plan features" .value=${planFeatures} disabled ></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Roles" .value=${roles} readonly></gui-tags> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-tags label="Plan features" value='["SSO"]' disabled></gui-tags> </div> <div class="gui-grid__cell"> <gui-tags label="Roles" value='["Admin", "Billing"]' readonly></gui-tags> </div></div>Errors
Section titled “Errors”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:
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.'] : [], ) } /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiTagsEvents } from '@golemui/gui-components';import '@golemui/gui-components/tags';
type TagsEvent<K extends keyof typeof GuiTagsEvents> = GuiEventMap< typeof GuiTagsEvents>[K];
@Component({ selector: 'app-tags-validation', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-tags id="interests-tags" label="Interests" hint="Up to three." [errors]="errors()" (gui-change)="onChange($event)" ></gui-tags> `,})export class TagsValidationComponent { errors = signal<string[]>([]);
onChange(event: Event) { const interests = (event as TagsEvent<'gui-change'>).detail.value ?? []; this.errors.set( interests.length > 3 ? ['Keep it to three interests.'] : [], ); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
const errors = ref<string[]>([]);
function onChange(interests: string[] | undefined) { errors.value = (interests?.length ?? 0) > 3 ? ['Keep it to three interests.'] : [];}</script>
<template> <gui-tags id="interests-tags" label="Interests" hint="Up to three." :errors.prop="errors" @gui-change="onChange($event.detail.value)" ></gui-tags></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiTagsEvents } from '@golemui/gui-components';import '@golemui/gui-components/tags';import '@golemui/gui-components/index.css';
type TagsEvent<K extends keyof typeof GuiTagsEvents> = GuiEventMap< typeof GuiTagsEvents>[K];
@customElement('tags-validation')export class TagsValidation extends LitElement { @state() private errors: string[] = [];
protected override createRenderRoot() { return this; }
private onChange(event: TagsEvent<'gui-change'>) { this.errors = (event.detail.value ?? []).length > 3 ? ['Keep it to three interests.'] : []; }
override render() { return html` <gui-tags id="interests-tags" label="Interests" hint="Up to three." .errors=${this.errors} @gui-change=${this.onChange} ></gui-tags> `; }}<gui-tags id="interests-tags" label="Interests" hint="Up to three."></gui-tags>
<script type="module"> const interests = document.querySelector('#interests-tags');
interests.addEventListener('gui-change', (event) => { interests.errors = event.detail.value.length > 3 ? ['Keep it to three interests.'] : []; });</script>In a form
Section titled “In a form”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/tags';import '@golemui/gui-components/textinput';
@Component({ selector: 'app-tags-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="post-form" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput label="Title" name="title" value="Notes on the Analytical Engine" ></gui-textinput> </div> <div class="gui-grid__cell"> <gui-tags label="Tags" name="tags" required></gui-tags> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Publish"></gui-button> </div> </div> <p id="post-result" role="status">{{ result() }}</p> </form> `,})export class TagsNativeFormComponent { result = signal('');
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); const data = new FormData(form); // getAll: the tags field submits one entry per tag. const post = { title: data.get('title'), tags: data.getAll('tags') }; this.result.set(`Submitted ${JSON.stringify(post)}`); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/button';import '@golemui/gui-components/tags';import '@golemui/gui-components/textinput';import '@golemui/gui-components/index.css';
const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; 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') }; result.value = `Submitted ${JSON.stringify(post)}`;}</script>
<template> <form id="post-form" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput label="Title" name="title" value="Notes on the Analytical Engine" ></gui-textinput> </div> <div class="gui-grid__cell"> <gui-tags label="Tags" name="tags" :required="true"></gui-tags> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Publish"></gui-button> </div> </div> <p id="post-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import '@golemui/gui-components/button';import '@golemui/gui-components/tags';import '@golemui/gui-components/textinput';import '@golemui/gui-components/index.css';
@customElement('tags-native-form')export class TagsNativeForm extends LitElement { @query('#post-form') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); const data = new FormData(this.form); // getAll: the tags field submits one entry per tag. const post = { title: data.get('title'), tags: data.getAll('tags') }; this.result = `Submitted ${JSON.stringify(post)}`; }
override render() { return html` <form id="post-form" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput label="Title" name="title" value="Notes on the Analytical Engine" ></gui-textinput> </div> <div class="gui-grid__cell"> <gui-tags label="Tags" name="tags" required></gui-tags> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Publish"></gui-button> </div> </div> <p id="post-result" role="status">${this.result}</p> </form> `; }}<form id="post-form"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput label="Title" name="title" value="Notes on the Analytical Engine"></gui-textinput> </div> <div class="gui-grid__cell"> <gui-tags label="Tags" name="tags" required></gui-tags> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Publish"></gui-button> </div> </div> <p id="post-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#post-form');
form.addEventListener('submit', (event) => { event.preventDefault(); const data = new FormData(form); // getAll: the tags field submits one entry per tag. const post = { title: data.get('title'), tags: data.getAll('tags') }; document.querySelector('#post-result').textContent = `Submitted ${JSON.stringify(post)}`; });</script>How the elements submit, validate and reset is in Native forms.
gui-pills
Section titled “gui-pills”<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. |
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<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 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>| Class | Part |
|---|---|
gui-field | The element itself. Lines its parts up with the fields next to it in a gui-grid row. |
gui-label | The label, with the text and the hint. |
gui-label__text | The label text and the required marker. |
gui-widget-hint | The hint. |
gui-widget | The box around the field. |
gui-widget-input | The part that looks like a field: the tags and the input. |
gui-tags-input | The same part: the container the pills measure to collapse into a count. |
gui-tags-input--icon | On that part when there is an icon. |
gui-widget-icon | The icon, with your icon classes. |
gui-tags__pills | The gui-pills element. |
gui-pills__strip-wrapper | The box of the strip, hidden while the tags show as a count. |
gui-pills--start-shadow, gui-pills--end-shadow | On the box while tags are scrolled out of view at either end: they fade that end. |
gui-pills__strip | The row of pills, which scrolls when they don’t fit. |
gui-pills__pill | A tag. |
gui-pills__pill-text | The text of a tag. |
gui-pills__pill-remove | The remove button of a tag. |
gui-pills__compact | The box of the count, shown while the field is narrow. |
gui-pills__count | The count of tags. |
gui-pills__dropdown | The list of tags the count opens. |
gui-pills__dropdown-list | The tags in that list. |
gui-tags__input | The text input. |
gui-validator | The errors container. It is always there, so the alert role announces new errors. |
gui-validator__error | One 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.
Tokens
Section titled “Tokens”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:
| Token | What it styles |
|---|---|
--gui-widget-size | The least height of the field, and the size of the count’s box. |
--gui-radius-md | The corner radius of the field, the pills and the tags list. |
--gui-bg-surface | The field’s background. |
--gui-border-default | The field’s border, the line after the pills, and the tags list’s border. |
--gui-text-default | The typed text and the label. |
--gui-font-family | The font. |
--gui-font-sm | The text size of the label and the field. |
--gui-font-xs | The text size of the hint, the errors and the count. |
--gui-text-secondary | The hint. |
--gui-intent-primary | The pills’ and the count’s background. |
--gui-color-white | The pills’ and the count’s text. |
--gui-pill-height | The height of each pill. |
--gui-pill-font-size | The text size of the pills. |
--gui-pill-action-size | The size of the remove icon. |
--gui-pill-action-hit | The clickable area of the remove button. |
--gui-space-2 | The space between the pills, and around the input. |
--gui-bg-default | The tags list’s background, and the fade at the strip’s ends. |
--gui-shadow-md | The tags list’s shadow. |
--gui-border-focus | The field’s border while the input or a pill has focus. |
--gui-shadow-focus | The focus ring, around the field and the focused pill. |
--gui-intent-error | The field’s border and the messages when there are errors. |
--gui-shadow-focus-error | The focus ring when there are errors. |
--gui-bg-disabled | The background when disabled. |
--gui-text-disabled | The text when disabled. |
--gui-space-1 | The space between the label, hint and errors. |
/* 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.
Accessibility
Section titled “Accessibility”- Name. The
labelnames the input and the group around the tags, througharia-labelledby. Without a label the field has no accessible name, and development builds log a warning. - Description. The
hintis the description of the group, througharia-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-labelreplaces 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 witharia-errormessage, and screen readers announce new errors as they appear. - Required and read-only.
requiredsetsaria-requiredon 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:
| Key | Action |
|---|---|
| Enter , | Adds the typed text as a tag, with the default separators. |
| Tab | Adds the typed text as a tag, and moves focus on. |
| Escape | Clears the typed text. |
| Backspace | In 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:
| Key | Action |
|---|---|
| ← → | 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 End | Moves to the first or last tag. |
| Delete Backspace | Removes the tag, and fires gui-change. |
| Escape | In 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.