@get-set/gs-tags
v1.0.2
Published
Get-Set Tags  accessible tag / token input
Maintainers
Readme
@get-set/gs-tags
A dependency-free, fully accessible (WAI-ARIA) tag / token input available in two flavours from one codebase:
- Native / vanilla JS — a
window.GSTags(selectorOrElement, params)factory that enhances an<input>(also exposed as a jQuery plugin and anHTMLElement.prototypemethod). - React — a
<GSTags />forwardRefcomponent with a typedGSTagsHandle(add/remove/clear/getTags/setTags/focus/blur/destroy).
Both surfaces drive one shared core (actions/, constants/, helpers/,
types/), so behaviour is identical across the two — every option works in
both targets (except the few tagged React-only below).
Features
- Rich delimiter set — commit tags on
Enter,,,;, space,Tab, blur, or by splitting pasted text (delimiters+pasteSplit). - Validation pipeline —
maxTags,allowDuplicates,validate(RegExp | string | predicate),allowedPattern(per-keystroke block),restrictToSuggestions, withonInvalidreporting the exact reason. - Transforms —
trim,lowercase,uppercase,capitalize(single or ordered list) applied before a value becomes a tag. - Autocomplete —
suggestionsas a static array or an async resolver (debounced), with keyboard navigation,minChars,maxSuggestionsand hide-already-selected. - Interactions — inline editable tags (double-click), per-tag remove ×, drag-to-reorder, and Backspace-removes-last.
- Beautiful motion — spring tag pop-in / pop-out, reorder transitions and
a dropdown reveal, all honoring
prefers-reduced-motion. - Accessible —
role="list"/listitem,combobox+listbox/optionwiring,aria-expanded/aria-autocomplete, labelled remove buttons and full keyboard support. - Light / dark / auto theming +
accentColor, three sizes (sm/md/lg), RTL,disabled/readOnly, hidden-input form submission (name), per-partcustomClass, and a React-only scoped-stylesgsxprop.
Compatibility
| Target | Requirement |
|---|---|
| React (<GSTags>) | React 16.8+ (Hooks + forwardRef), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. |
| Native / vanilla (window.GSTags) | No framework. Any modern evergreen browser. Optional jQuery ($(...).GSTags(...)) and HTMLElement.prototype.GSTags(...) adapters are registered automatically by the bundle. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import; render <GSTags> inside a Client Component ('use client'). |
Project layout
GSTags.ts # native entry (webpack -> dist-js/bundle.js, window global)
components/GSTags.tsx # React component (tsc -> dist/, npm entry)
actions/ # shared engine (init/mutate/render/suggestions/destroy/refresh)
constants/ # default params + sizes
helpers/ # tags (validation/transform/split), keys, layout, ui helpers
types/ # Params / Ref / Window augmentation
components/styles/ # SCSS + compiled CSS + CSS-as-TS (runtime injection for React)
styles/ # SCSS + compiled CSS (for <link> use by the native build)Install
npm install @get-set/gs-tagsBuild & test
npm install
npm run build # builds both targets
npm run build:js # native bundle -> dist-js/bundle.js
npm run build:react # React + types -> dist/
npm test # vitestSee example.html for a runnable native showcase.
Usage — Native JS
<input id="skills" placeholder="Add a skill…" />
<link rel="stylesheet" href="node_modules/@get-set/gs-tags/styles/GSTags.css" />
<script src="node_modules/@get-set/gs-tags/dist-js/bundle.js"></script>
<script>
// The factory accepts a CSS selector STRING **or** a DOM element.
const tags = new GSTags('#skills', {
value: ['react', 'typescript'],
placeholder: 'Add a skill…',
maxTags: 8,
delimiters: ['enter', 'comma', 'paste'],
transform: 'lowercase',
suggestions: ['react', 'vue', 'svelte', 'angular', 'solid'],
onChange: (t) => console.log('tags:', t)
});
// Imperative control:
// tags.add('graphql'); tags.remove('vue'); tags.clear();
// tags.getTags(); tags.setTags(['a','b']); tags.focus();
</script>Passing a DOM element (also what the adapters do)
const el = document.querySelector('#skills');
const tags = new GSTags(el, { maxTags: 5 }); // element, not a selectorInstance registry
Every instance is registered under its reference (auto-generated when omitted):
new GSTags('#skills', { reference: 'skills' });
const inst = window.GSTagsConfigue.instance('skills');
inst.add('rust');Usage — jQuery
Registered automatically by the bundle when jQuery is present:
$('#skills').GSTags({ maxTags: 5, delimiters: ['enter', 'comma'] });Usage — HTMLElement.prototype
Also registered by the bundle:
document.querySelector('#skills').GSTags({ suggestions: ['a', 'b', 'c'] });Usage — React
'use client';
import { useRef } from 'react';
import GSTags, { GSTagsHandle } from '@get-set/gs-tags';
import '@get-set/gs-tags/styles/GSTags.css'; // or rely on the auto-injected styles
export default function Example() {
const ref = useRef<GSTagsHandle>(null);
return (
<>
<GSTags
ref={ref}
defaultValue={['react', 'typescript']}
placeholder="Add a skill…"
maxTags={8}
delimiters={['enter', 'comma', 'paste']}
transform="lowercase"
suggestions={['react', 'vue', 'svelte', 'angular', 'solid']}
theme="auto"
onChange={(tags) => console.log(tags)}
/>
<button onClick={() => ref.current?.add('graphql')}>Add graphql</button>
<button onClick={() => ref.current?.clear()}>Clear</button>
</>
);
}Controlled mode
const [tags, setTags] = useState<string[]>([]);
<GSTags value={tags} onChange={setTags} />Async suggestions
<GSTags
suggestions={async (query) => {
const res = await fetch(`/api/tags?q=${encodeURIComponent(query)}`);
return res.json(); // string[] | { value, label }[]
}}
minChars={2}
suggestionDebounce={250}
/>Imperative handle — GSTagsHandle
| Method | Signature | Description |
|---|---|---|
| add | (value: string \| string[]) => void | Add one or more tags (validated). |
| remove | (valueOrIndex: string \| number) => void | Remove a tag by value or index. |
| clear | () => void | Remove every tag. |
| getTags | () => string[] | Read the current tags (a copy). |
| setTags | (tags: string[]) => void | Replace the whole set (validated). |
| focus | () => void | Focus the text input. |
| blur | () => void | Blur the text input. |
| destroy | () => void | Unregister + remove scoped gsx styles. |
The native instance exposes the same methods (chainable) plus
setDisabled(on), refresh() and destroy():
inst.add('a').add(['b', 'c']).remove('b'); // chainable
inst.getTags(); inst.setTags(['x']); inst.focus();
inst.setDisabled(true); inst.refresh(); inst.destroy();React-only: gsx scoped styles
<GSTags
gsx={{
'.gs-tags-tag': { borderRadius: '999px' },
'.gs-tags-remove:hover': { background: 'crimson' }
}}
/>gsx injects a <style data-key="…"> scoped to that instance and removes it on
unmount. It is a React-only convenience and has no native equivalent.
Props
Every option below works in both the native factory and the React component unless tagged (React-only).
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string[] | [] | Initial tags (native seed). In React, the controlled tag list. |
| defaultValue (React-only) | string[] | [] | Uncontrolled initial tags. |
| placeholder | string | '' | Placeholder in the text input. |
| maxTags | number \| false | false | Maximum number of tags. |
| allowDuplicates | boolean | false | Allow the same value more than once. |
| delimiters | GSTagsDelimiter[] | ['enter','comma'] | Keys / actions that commit the pending text. |
| pasteSplit | RegExp \| string | /[,\n;]+/ | Splits pasted text into multiple tags. |
| validate | RegExp \| string \| (value, tags) => boolean | — | Reject a candidate before it becomes a tag. |
| allowedPattern | RegExp \| string | — | Characters allowed while typing (per-keystroke block). |
| transform | GSTagsTransform \| GSTagsTransform[] | — | Normalise a value before committing. |
| editableTags | boolean | false | Double-click a tag to edit inline. |
| removable | boolean | true | Render an × remove button per tag. |
| draggable | boolean | false | Drag tags to reorder. |
| backspaceRemoves | boolean | true | Backspace on an empty field removes the last tag. |
| addOnBlur | boolean | false | Commit the pending text when the field blurs. |
| suggestions | string[] \| (query) => (string \| {value,label})[] \| Promise<…> | — | Autocomplete source (static or async). |
| minChars | number | 0 | Minimum query length before suggesting. |
| restrictToSuggestions | boolean | false | Only allow values from the suggestions list. |
| maxSuggestions | number | 8 | Cap the number of suggestions shown. |
| hideSelectedSuggestions | boolean | true | Hide already-selected values from the dropdown. |
| suggestionDebounce | number | 200 | Debounce (ms) for async suggestion queries. |
| disabled | boolean | false | Disable all interaction. |
| readOnly | boolean | false | Tags visible but not editable. |
| autofocus | boolean | false | Focus the input on mount. |
| name | string | — | Emit a hidden name[] input per tag for form submission. |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme. |
| accentColor | string | #2563eb | Accent color (CSS var --gst-accent). |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Control size preset. |
| rtl | boolean | false | Right-to-left layout. |
| reducedMotion | 'auto' \| boolean | 'auto' | Honor prefers-reduced-motion. |
| customClass | GSTagsCustomClass | — | Per-part class overrides. |
| className | string | — | Extra class on the root. |
| gsx (React-only) | NestedCSS | — | Scoped inline styles for this instance. |
| reference | string | auto | Registry key (auto-generated when omitted). |
| onAdd | (value, tags) => void | — | After a tag is added. |
| onRemove | (value, tags) => void | — | After a tag is removed. |
| onChange | (tags) => void | — | Whenever the tag set changes. |
| onInvalid | (value, reason) => void | — | When a candidate is rejected. |
Catalogs (every enum value)
delimiters — GSTagsDelimiter
| Value | Commits on |
|---|---|
| enter | The Enter key. |
| comma | Typing ,. |
| space | The space bar. |
| semicolon | Typing ;. |
| tab | The Tab key (commits, then moves focus). |
| blur | Losing focus (same as addOnBlur). |
| paste | Splitting pasted text (via pasteSplit). |
transform — GSTagsTransform
| Value | Effect |
|---|---|
| trim | Strip leading/trailing whitespace. |
| lowercase | value.toLowerCase(). |
| uppercase | value.toUpperCase(). |
| capitalize | Upper-case the first letter of each word. |
onInvalid reasons — GSTagsInvalidReason
duplicate · maxTags · pattern (failed allowedPattern) · validate
(failed validate / restrictToSuggestions) · empty.
theme — GSTagsTheme
light · dark · auto (follows prefers-color-scheme).
size — GSTagsSize
sm · md · lg.
customClass — GSTagsCustomClass
root · tag · tagLabel · remove · input · dropdown · option.
Keyboard
| Key | Action |
|---|---|
| Enter / , / ; / space / Tab | Commit the pending text (per delimiters). |
| Backspace (empty field) | Remove the last tag. |
| ↑ / ↓ | Move through the suggestion dropdown. |
| Enter (with active suggestion) | Accept the highlighted suggestion. |
| Escape | Close the suggestion dropdown. |
| Double-click a tag | Edit it inline (editableTags). |
License
ISC © Get-Set
