Skip to content
ShineUI
Install

InputTags

Free entry, chip by chip.

<sui-input-tags>Text inputsStable

About

Free-form entry that turns each typed value into a removable chip: recipients, report tags, short labels. Enter or the separator commits the text, Backspace on an empty box removes the last chip, and blur commits what is pending. Form-associated: it posts one name entry per chip.

Usage

<sui-input-tags label="Send copies to" name="cc" help="Press Enter after each address."></sui-input-tags> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-input-tags"); el.value = ["dana@fallscitysupply.com"]; /* SuiInputTagsValue[] */ el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiInputTags } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const value = ["dana@fallscitysupply.com"]; /* SuiInputTagsValue[] */ return <SuiInputTags label="Send copies to" name="cc" help="Press Enter after each address." value={value} onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiInputTags } from "@shineui/vue"; import "@shineui/core/shineui.css"; const value = ["dana@fallscitysupply.com"]; // SuiInputTagsValue[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiInputTags label="Send copies to" name="cc" help="Press Enter after each address." :value="value" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiInputTags } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiInputTags], template: `<sui-input-tags label="Send copies to" name="cc" help="Press Enter after each address." [value]="value" (suiChange)="onChange($event)"></sui-input-tags>`, }) export class Example { value = ["dana@fallscitysupply.com"]; // SuiInputTagsValue[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

value is a property, so set it from script. Each add, remove or refusal is announced through a polite live region.

<div style="max-width:420px"> <sui-input-tags id="tags-basic" label="Send copies to" name="cc" help="Press Enter after each address."></sui-input-tags> </div>
// setup(root): properties and events setup(root) { root.querySelector('#tags-basic').value = ['dana@fallscitysupply.com']; }

Sizes

The size is the box height before it wraps: md is the default, one height with every select, picker and button; lg is the roomier box for a sign-in or marketing page; sm packs into table rows. The chips stay small at every size.

<div style="display:grid;gap:14px;max-width:420px"> <sui-input-tags id="tags-lg" label="Large" size="lg"></sui-input-tags> <sui-input-tags id="tags-md" label="Medium" size="md"></sui-input-tags> <sui-input-tags id="tags-sm" label="Small" size="sm"></sui-input-tags> </div>
// setup(root): properties and events setup(root) { for (const id of ['tags-lg', 'tags-md', 'tags-sm']) root.querySelector(`#${id}`).value = ['urgent', 'invoice']; }

Tags with a value of their own

An entry may be a { label, value } tag, so the chip reads a person's name while the field posts their address. Text the user types is committed as a plain string, since the field has no identity to give it, and a duplicate is judged on the posted value: typing ada@fallscitysupply.com is refused here.

Submit to see what posts.
<form id="tags-obj-form" style="display:grid;gap:10px;max-width:460px"> <sui-input-tags id="tags-obj" label="Send copies to" name="cc" help="Type an address and press Enter."></sui-input-tags> <sui-button type="submit" size="sm" variant="outline" label="Show what posts"></sui-button> <output id="tags-obj-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#tags-obj-form'); const out = root.querySelector('#tags-obj-out'); root.querySelector('#tags-obj').value = [ { label: 'Ada Byrne', value: 'ada@fallscitysupply.com' }, { label: 'Grace Whitfield', value: 'grace@fallscitysupply.com' }, ]; form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v}`).join(' '); }); }

Limits and states

max caps the count and refuses further entries; a duplicate is refused too. A disabled field takes no typing, removes no chip and posts nothing.

<div style="display:grid;gap:14px;max-width:420px"> <sui-input-tags label="Recipients" required error="Add at least one recipient."></sui-input-tags> <sui-input-tags id="tags-max" label="Labels" max="3" help="Up to three. Paste a comma-separated list."></sui-input-tags> <sui-input-tags id="tags-off" label="Locked" disabled></sui-input-tags> </div>
// setup(root): properties and events setup(root) { root.querySelector('#tags-max').value = ['urgent', 'invoice']; root.querySelector('#tags-off').value = ['finance']; }

Separator

Semicolon commits, so a value may contain a comma. Type an address and end it with a semicolon.

<div style="max-width:420px"> <sui-input-tags id="tags-sep" label="Ship to" separator=";" help="End each line with a semicolon."></sui-input-tags> </div>
// setup(root): properties and events setup(root) { root.querySelector('#tags-sep').value = ['120 Main St, Falls City', '4402 Dock Rd, New Albany']; }

Read-only

readonly keeps the chips readable, drops their remove control, refuses new entries, and still posts them.

<div style="max-width:420px"><sui-input-tags id="tags-ro" label="Report tags" name="tags-ro" readonly></sui-input-tags></div>
// setup(root): properties and events setup(root) { root.querySelector('#tags-ro').value = ['month-end', 'board pack']; }

In a form

Each chip posts one entry under the host name; an empty field posts no entry at all. Reset restores the chips the field mounted with.

Submit to see what posts.
<form id="tags-form" style="display:grid;gap:14px;max-width:420px"> <sui-input-tags id="tags-form-cc" label="Send copies to" name="cc" required></sui-input-tags> <sui-input-tags label="Report tags" name="tag"></sui-input-tags> <div style="display:flex;gap:8px"> <sui-button type="submit" variant="brand" label="Send report"></sui-button> <sui-button type="reset" variant="outline" label="Reset"></sui-button> </div> <output id="tags-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#tags-form'); const out = root.querySelector('#tags-out'); root.querySelector('#tags-form-cc').value = ['dana@fallscitysupply.com']; form.addEventListener('submit', (e) => { e.preventDefault(); const entries = [...new FormData(form)]; out.textContent = entries.length ? entries.map(([k, v]) => `${k}=${v}`).join(' ') : 'Nothing posts.'; }); form.addEventListener('reset', () => { out.textContent = 'Reset. Submit to see what posts.'; }); }

Events

suiChange fires when a chip is added or removed, with the whole array. suiInput fires as the pending text changes, and with the remainder after a commit. suiFocus and suiBlur follow the entry box; a blur commits the pending text first, so suiChange comes before suiBlur.

Idle.
<div style="display:grid;gap:10px;max-width:420px"> <sui-input-tags id="tags-events" label="Report tags" name="tag"></sui-input-tags> <span id="tags-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#tags-log'); const el = root.querySelector('#tags-events'); el.addEventListener('suiFocus', () => { log.textContent = 'suiFocus'; }); el.addEventListener('suiBlur', () => { log.textContent = 'suiBlur'; }); el.addEventListener('suiChange', (e) => { log.textContent = `suiChange [${e.detail.join(', ')}]`; }); el.addEventListener('suiInput', (e) => { log.textContent = `suiInput ${e.detail || '(empty)'}`; }); }

Keyboard

KeyAction
EnterCommits the typed text as a chip.
SeparatorA comma by default. Commits the text before it and leaves the rest in the box.
BackspaceOn an empty box, removes the last chip.
TabReaches each chip's remove control, labeled "Remove" and the chip's own text, and then the box.

API

<sui-input-tags>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables the field: no typing, no chip removal, and the element posts nothing.
errorerrorstringstring | undefinedError message; paints the field in the danger tone and sets aria-invalid.
helphelpstringstring | undefinedHelp text under the field. Replaced by error when set.
invalidinvalidbooleanfalseDanger border without a message (validation handled elsewhere).
labellabelstringstring | undefinedLabel above the field. Without one the box and its entry field take the host's aria-label or aria-labelledby as their name; a visible label wins over both.
maxmaxnumbernumber | undefinedMaximum number of chips; further entries are ignored. Non-positive or fractional values are floored, and anything below 1 means no limit.
namenamestringstring | undefinedForm control name; each chip posts one entry under it. Reflected, so named form access (form.elements[name]) works when a framework binding sets the property.
placeholderplaceholderstringstring | undefinedPlaceholder shown while no chip exists.
readOnlyreadonlybooleanfalseDisplay only: the chips can be read and copied but not added to or removed; the field still posts them.
requiredrequiredbooleanfalseAt least one chip is required for the surrounding form to be valid.
separatorseparatorstring','Character (or string) that commits the typed text, and the one a paste is split on. Defaults to a comma; set separator=";" for values that legitimately contain commas, such as address lines. An empty separator falls back to the comma rather than splitting every character.
sizesize"lg" | "md" | "sm"'md'Box height before it wraps: sm 28 for table rows, md 34 for forms and toolbars (the default, shared with the select family and the buttons, so a form built from defaults is one height), lg 42 for a sign-in or marketing page. The chips stay small at every size.
valueproperty onlySuiInputTagsValue[][]Committed values, one per chip: plain strings, or { label, value } tags where the chip text and the posted value differ. The two may be mixed, because text the user types is committed as a string: the field has no identity to give it that it was not given. A duplicate is refused on the posted value, so typing an address already held under a person's name is refused. Mutable; updated before suiChange fires. Property, not attribute.

Events

EventDetailReactVueDescription
suiBlurvoidonSuiBlur@sui-blurFires when the entry box loses focus, a move to a chip's remove control included, after the pending text is committed; no detail. Does not bubble.
suiChangeSuiInputTagsValue[]onSuiChange@sui-changeFires when a chip is added or removed; detail is the new value, the full array in the shape it was given (strings, tags, or both).
suiFocusvoidonSuiFocus@sui-focusFires when the entry box takes focus; no detail. The same pair is on every text field. Does not bubble.
suiInputstringonSuiInput@sui-inputFires when the pending text changes: on each keystroke, and with the remainder after a commit. Does not fire when a chip is removed.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus into the entry box, as el.focus() does.

Types

SuiInputTagsItem interfaceA tag with an identity of its own: label is the chip text, value is what the field posts and what duplicates are judged on. Use it when the chip a person reads is not the value a form needs, as a recipient's name is not their address. The value stays as given, so a numeric id comes back a number; the form posts its text.
labelstring
valueSuiKey
SuiInputTagsValue typeOne entry of value: a plain string, or a { label, value } tag.
No members.
SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also