GrInputTag
Lets you enter and edit a list of tags or values.
Overview
When to take it
- the values are invented by the user — the tags of an article, keywords, mailing addresses: there is no reference list;
- the values are pasted in bulk —
separatorssplits a string pasted from the clipboard into chips; - what is added has to be checked —
beforeAddrejects an invalid address or a duplicate before a chip appears; - the number is limited —
maxdoes not let an extra one be added.
When to take something else
| Need | Take |
|---|---|
| There is a reference list of values | GrSelect with multiple and tags |
| The reference list is large, the values are searched by typing | GrAutocomplete |
| There is a single value | GrInput |
| The values are nested | GrTreeSelect |
Variants every tone and view, rendered by the real component
state
defaultsuccesswarningdanger
Sizes
xssmmdlg
States
normaldisabledloadingreadonlyinvalid
The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrInputTag v-model="value" />Examples 3 each with a live demo and its code
Invites from a pasted list
A line pasted from a spreadsheet becomes separate addresses:
separatorssplits on commas, spaces and semicolons, andadd-on-blurkeeps what was typed but not confirmed.before-addstops a malformed address or a current member before it turns into a chip,maxstops the set where the plan stops, andnamesends each address with a native form.Invite people to Northwind Traders 2 of 8 · paste a list or press Enter
Keywords fixed in place
editablefixes «acessibility» where it stands — double-click it or press F2 on it — instead of «delete and type again», andeditreports what changed. The#sits in the prefix addon, andtag-tonesets the chips apart from the text around them.Tokens are roles, not coloursBlog post · draft Double-click a keyword or press F2 on it to fix a typo.#design-tokensdark-modeacessibilityvueDomains checked by the server
An async
before-addraisesloadingby itself while the server looks for the verification record, and a domain without one never becomes a chip. Thetagslot fills the chip — a «verified» mark before the domain — while the chip, itstag-toneand its remove button stay the component’s.Single sign-onPeople with these email domains sign in through Okta. northwind.example, verified
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | Switch the field off: tags are neither added nor removed. Combined with `GrFormField`. |
tagTone |
| "neutral" | The tone of the tag chips. `neutral` by default. |
tagDark | boolean | undefined | false | Chips with a fill in the tone instead of a soft surface. `false` by default. |
tagSize | "xs" | "sm" | "md" | "lg" | undefined | "md" | The type size of the chips by the `GrBadge` scale — the same step as the tags of `GrSelect`. `md` by default. |
tagRadius | GrBadgeRadius | undefined | "round" | The shape of the chips: `round`, `semi` or `square`. `round` by default. |
readonly | boolean | undefined | false | Read-only: the tags are visible, but they cannot be added, removed or cleared. |
invalid | boolean | undefined | false | A validation error: a danger border and `aria-invalid`. Combined with the error of `GrFormField`. |
required | boolean | undefined | false | A required field (`aria-required`). It adds to the `required` of `GrFormField`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The height and the type size by the controls scale. `md` by default; taken from `GrConfigProvider`. |
placeholder | string | undefined | undefined | A hint in an empty field — it disappears with the first tag. It does not replace the label: the name is given by `GrFormField` or `ariaLabel`. |
ariaLabel | string | undefined | undefined | The name of the control when it is used outside `GrFormField`. Inside a field the name comes from the pairing with its `<label for>` — there the prop is not needed. Without either the input has no accessible name at all: a placeholder does not count as a name. |
clearable | boolean | undefined | undefined | A "remove all tags" button. It is configured through `GrConfigProvider`. |
loading | boolean | undefined | false | Background work: a spinner plus `aria-busy`. An asynchronous `beforeAdd` raises it itself. |
name | string | undefined | undefined | The name for a native form: a hidden input per tag. |
max | number | undefined | undefined | The limit on the number of tags: beyond it tags are not added, and a screen reader hears that the limit has been reached. |
trim | boolean | undefined | true | Trim the spaces at the edges of a tag. `true` by default. |
state | "default" | "success" | "warning" | "danger" | undefined | "default" | Border highlighting at the developer's discretion. `success` and `warning` add an icon and a hidden label; `invalid` is stronger. `default` by default. |
separators | string[] | undefined | [","] | The keys that turn the typed text into a tag — a paste is split by them as well. `Enter` always works. `[',']` by default. |
allowDuplicates | boolean | undefined | false | Allow identical tags. By default a repeat is silently not added. |
addOnBlur | boolean | undefined | false | Unfinished text becomes a tag when the focus leaves rather than disappearing. `false` by default. |
clearInputOnAdd | boolean | undefined | true | Clear the input after a tag is added. `true` by default. |
beforeAdd | ((tag: string) => boolean | Promise<boolean>) | undefined | undefined | A check of a tag before adding. It may be asynchronous (a check on the server). A rejected tag is not added and leaves in the `reject` event. |
editable | boolean | undefined | false | Editing a tag in place: `F2` on a chip or a double click on it open the field, `Enter` confirms, `Escape` cancels. A prop rather than the default behaviour: today a double click on a chip does nothing, and turning it into an entry into editing for all consumers would be a silent change of behaviour. |
tagClosable | boolean | undefined | true | A remove cross on every chip. `true` by default; without it a tag is removed only with `Backspace` from an empty field, and editing (`editable`) is unavailable from the keyboard. |
removeTagLabel | string | undefined | undefined | i18n-friendly aria-label for the per-tag remove button. |
clearAllLabel | string | undefined | undefined | The i18n aria-label of the "remove all" button. |
modelValuerequiredv-model | string[] | — | The set of tags (`v-model`). The component does not mutate it — it emits a new array. |
prefixMinWidth | string | undefined | — | The widths of the `prefix`/`suffix` addons — the shared contract of the controls of the package (`docs/form-controls.md`). |
prefixMaxWidth | string | undefined | — | The maximum width of the prefix. |
suffixMinWidth | string | undefined | — | The minimum width of the suffix. By default — by the size step. |
suffixMaxWidth | string | undefined | — | The maximum width of the suffix. |
prefixFixed | boolean | undefined | — | A fixed width of the prefix (`prefixMaxWidth` → `prefixMinWidth` → by the step) — so that the addons of neighbouring fields line up in an even column. |
suffixFixed | boolean | undefined | — | A fixed width of the suffix — as with `prefixFixed`. |
Slots
| Slot | Type | Description |
|---|---|---|
prefix | any | An addon to the left of the chips: an icon, a label. |
suffix | any | An addon on the right, before the clear button. |
tag | { tag: string; index: number; remove: () => void; } | The content of a chip: the label, an icon, a counter. The chip itself, its tone and the remove button stay with the component; `remove` is for your own actions inside the label. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: string[]): void | — |
change | (event: "change", value: string[]): void | — |
clear | (event: "clear"): void | — |
focus | (event: "focus", event: FocusEvent): void | — |
blur | (event: "blur", event: FocusEvent): void | — |
add | (event: "add", value: string): void | — |
remove | (event: "remove", value: string, index: number): void | — |
reject | (event: "reject", value: string): void | — |
edit | (event: "edit", value: string, index: number, previous: string): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
focus | () => void | Focus into the tag input. |
blur | () => void | Remove the focus from the input. |
clear | () => void | Remove all the tags, as the clear button does, and return the focus to the field. Does nothing in `disabled` and `readonly`. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrInputTagConfigurableProps, GrInputTagProps, GrInputTagEmits } from '@feugene/granularity/components/GrInputTag'Filedist/types/components/GrInputTag/config.d.ts
const grInputTagConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrInputTag">;Filedist/types/components/GrInputTag/defaults.d.ts
interface GrInputTagConfigurableProps {
size: GrComponentSize;
clearable: boolean;
}Filedist/types/components/GrInputTag/GrInputTag.vue.d.ts
interface GrInputTagProps {
modelValue: string[];
placeholder?: string;
disabled?: boolean;
readonly?: boolean;
invalid?: boolean;
required?: boolean;
state?: GrInputTagState;
size?: GrInputTagSize;
separators?: string[];
allowDuplicates?: boolean;
trim?: boolean;
max?: number;
addOnBlur?: boolean;
clearInputOnAdd?: boolean;
beforeAdd?: (tag: string) => boolean | Promise<boolean>;interface GrInputTagEmits {
(e: 'update:modelValue', value: string[]): void;
(e: 'add', value: string): void;
(e: 'remove', value: string, index: number): void;
(e: 'edit', value: string, index: number, previous: string): void;
(e: 'reject', value: string): void;
(e: 'clear'): void;
(e: 'change', value: string[]): void;
(e: 'focus', event: FocusEvent): void;
(e: 'blur', event: FocusEvent): void;
}Filedist/types/components/GrInputTag/grInputTagStyles.d.ts
type GrInputTagSize = GrInputSize;type GrInputTagState = GrControlState;Filedist/types/components/GrInputTag/safelist.d.ts
const grInputTagSafelist: string[];Filedist/types/components/GrInputTag/index.d.ts
type GrInputTagInstance = ComponentExposed<typeof GrInputTagComponent>;Accessibility
APG pattern
—
Keys
Delete / Backspace
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The keyboard: one tab stop for the whole set
The crosses of the chips live by a roving tabindex: there is exactly one button in the
tab order, and the rest are reachable with the arrows. Before that twenty tags gave
twenty-one Tab stops — the set was impossible to skip past.
| Key | Where | What it does |
|---|---|---|
Enter, a separator | the input field | add a tag |
Backspace | an empty input field | remove the last tag |
← | an empty input field | move to the last chip |
← / → | a chip | the previous / the next chip (past the last one — the input field) |
Home / End | a chip | the first / the last chip |
Delete, Backspace | a chip | remove the chip; the focus moves to a neighbour, and if no tags are left — into the field |
F2 | a chip | open editing (with editable) |
Enter | editing | save |
Escape | editing | cancel |
The name of a button names its own tag (“Remove the tag vue”) rather than a faceless “Remove the tag”: on twenty identical buttons there is otherwise no way to choose the right one.
The set of chips is declared a list (role="list" / listitem), so a screen reader reports
the number of tags. The container of the list is display: contents: the role is needed for
the semantics, it does not touch the layout, and the chips wrap in one flow with the field.
Editing a tag in place
editable opens editing with a double click on a tag or with F2 from the keyboard. Before
that a chip either existed or was removed as a whole: a typo in design-system-tokens-v2 cost
retyping it in full.
F2, not Enter. A chip has a single focusable node — the removal button, and Enter on it
already means “remove” by the native activation of a button. Hence the dependency as well: without
tagClosable editing cannot be entered from the keyboard at all, and in dev the console warns
about it.
The loss of focus saves. That is a deliberate divergence from addOnBlur (which is false by
default): it decides whether a half-typed new value becomes a tag, and discarding is safe
there — before that there was nothing. Editing, though, starts from an existing value, and quietly
reverting what was typed is exactly the outcome that would surprise. There is a way to back out:
Escape.
An empty value on Enter removes the tag, on the loss of focus it does not. Removal stays an
explicit action: “erased the text and got distracted” must not cost a tag.
beforeAdd checks the edit as well. The prop is named after adding, but a tag has one rule, and a
second predicate would diverge from the first; a rejection restores the previous value and is
announced by a live region. There are no separators in editing: one edit is one tag, otherwise a
comma would silently turn a tile into three.
max does not limit editing — the set does not grow from it.
Inside GrModal and the other layers Escape cancels the edit and does not close the window: the
editing session is registered in the shared stack of layers.
The limit of the set
max no longer blocks the field. It used to give the input a disabled at the limit: it
dropped out of the tab order and stopped accepting Backspace — the only way to remove a tag
from the keyboard. Now the field stays alive, extra tags simply are not added, and the
exhaustion of the limit is announced by a live region.
Adding and removing a tag are announced as well: without that a change of the set looked to a
blind user like “nothing happened”. All four announcements go into the shared live region of
the package — announcer.md; the component no longer has a role="status"
of its own.
The check before adding
<GrInputTag
v-model="emails"
:before-add="tag => /.+@.+\..+/.test(tag)"
@reject="showError"
/>beforeAdd may be asynchronous (a check on the server) — for the duration of the check a
spinner and aria-busy are raised. A second Enter cancels the previous check: the result of
the stale one is not appended. A rejected tag leaves in the reject event, so that the
consumer can explain the reason.
clearable and loading
clearable adds a “remove all” button (visible only when there are tags and the field is
editable) and a clear event. loading is the same spinner an asynchronous beforeAdd
raises, but under manual control.
The events
| Event | When |
|---|---|
update:modelValue | the set has changed |
add | a tag has been added |
remove | a tag has been removed, the arguments are the tag and its index |
reject | the tag did not pass beforeAdd |
clear | the set was removed with the button |
The imperative API
focus(), blur(), clear() through a ref on the component.
The states
state="success" and "warning" show an icon and a hidden label that goes into the
aria-describedby of the control: the colour of the border as the only carrier of meaning is
WCAG 1.4.1, and these two states have no ARIA equivalent of aria-invalid. danger gets no
icon — its meaning is already carried by aria-invalid and the error text from GrFormField —
and the mark goes out as soon as the control is declared invalid.
The size and the tokens
size is read from GrConfigProvider (the global size or a pointed componentDefaults),
and clearable has been moved there as well. A blocked field is dimmed with the --gr-muted
background and --gr-muted-fg text rather than with transparency: opacity dilutes tokens
tuned to AA. The cross of a chip inherits the colour of the badge tone at full strength —
dimming it with transparency is not allowed, on a dark chip the contrast fails.
The prefix / suffix addons
The slots put an icon, a unit or a label into the shell; the width is bounded by six props
(prefixMinWidth/prefixMaxWidth/prefixFixed and the same for the suffix). The shared
contract of the controls — form-controls.md.
The tag slot — the contents of the chip, not the chip
The slot draws the contents of the chip: the label, an icon, a counter. The chip itself
(GrChip), its tone and the remove button stay with the component — a chip of your own with a
cross inside the slot would give a chip within a chip and two crosses. remove in the slot props
is for your own actions inside the label, not for a second remove button.
<template>
<GrInputTag v-model="emails">
<template #tag="{ tag }">
<span class="font-500">{{ tag.split('@')[0] }}</span>
<span class="text-[var(--gr-muted-fg)]">@{{ tag.split('@')[1] }}</span>
</template>
</GrInputTag>
</template>The native form
The name prop renders an input[type="hidden"] per tag — the standard serialisation of a
set with a repeated key; an empty set sends nothing.
Props in this example