Accessibility-first tagging / multi-select input library for the web.
taga11y provides an accessible combobox component with inline chips, keyboard navigation, screen reader announcements, and native form integration. It follows the WAI-ARIA APG combobox pattern exactly, with aria-activedescendant keeping focus on the input during dropdown navigation.
<input> is enhanced in place and works as a form control without JavaScriptenforceSuggestions: trueprefers-color-scheme) or forced (data-theme)npm install taga11y
import Taga11y from 'taga11y';
import 'taga11y/dist/taga11y.css';
const widget = new Taga11y(inputEl, {
suggestions: ['JavaScript', 'TypeScript', 'Rust'],
maxTags: 10,
});
<script src="https://unpkg.com/taga11y/dist/taga11y.iife.js"></script>
<link rel="stylesheet" href="https://unpkg.com/taga11y/dist/taga11y.css">
<script>
const widget = new Taga11y(inputEl, {
suggestions: ['JavaScript', 'TypeScript'],
});
</script>
<label for="tags">Select tags:</label>
<input id="tags" type="text">
import Taga11y from 'taga11y';
import 'taga11y/dist/taga11y.css';
const input = document.querySelector('#tags');
const widget = new Taga11y(input, {
suggestions: [
'JavaScript',
'TypeScript',
'Rust',
'Go',
'Python',
],
maxTags: 10,
delimiter: [',', 'Enter'],
});
All options are optional.
| Option | Type | Default | Description |
|---|---|---|---|
suggestions |
SuggestionItem[] | { once } | { query } |
— | Suggestion source: static array, pre-fetched async ({ once: () => Promise<SuggestionItem[]> }), or dynamic per-keystroke ({ query: (input, signal) => Promise<SuggestionItem[]> }). Omit for free-text mode. |
maxTags |
number |
— | Maximum number of tags. No limit when omitted. |
delimiter |
string | string[] |
[',', 'Enter'] |
Character(s) that commit the current input as a tag. 'Tab' is a valid value. |
enforceSuggestions |
boolean |
false |
When true, only suggestions from the source can be committed. Free-text is rejected. |
name |
string |
inherited from input.name |
name on the hidden form input. The original input's name is removed on init (prevents double submission) and restored on destroy(). Override only when you need a different name. |
label |
string |
— | Injects a <label> inside the widget wrapper. Only use when no page-level <label for="…"> exists — existing page labels work automatically via the preserved id. Do not combine both. |
disabled |
boolean |
false |
Renders the component in a disabled state. |
theme |
'dark' | 'light' | null |
null |
Forces a colour scheme. null follows prefers-color-scheme. |
serialize |
(tags: TagData[]) => string |
comma-join values | Custom serializer for the hidden input value. |
deserialize |
(raw: string) => SuggestionItem[] |
split on ,, trim, drop empties |
Custom parser for the original input's value on init and on form reset. Pair with serialize to round-trip a non-comma format (e.g. JSON). String items resolve labels from loaded suggestions; { label, value } items are used as-is. |
debounceMs |
number |
200 |
Debounce delay (ms) for dynamic suggestion requests. Negative values are clamped to 0. |
i18n |
{ locale: string; dir?: 'ltr' | 'rtl'; strings?: Partial<I18nStrings> } |
— | Internationalisation. locale is a BCP 47 tag; dir (optional) stamps direction on the wrapper, otherwise it cascades; strings partially overrides the built-in English map (missing keys fall back to English with a dev console.warn). Init-only — ignored by settings(); switch locale via destroy() + new Taga11y(). See the i18n guide. |
The widget dispatches custom events on the original <input> element. All events bubble, are non-cancelable, and carry a detail payload. Use widget.on(name, handler) or inputEl.addEventListener(name, handler).
| Event | Detail | When fired |
|---|---|---|
taga11y:add |
{ tag: TagData } |
After a single tag is added (typing, suggestion select, addTag, addTags, or each chunk of a delimited paste). |
taga11y:remove |
{ tag: TagData } |
After a tag is removed (chip button, Backspace on empty input, or removeTag). |
taga11y:clear |
{ tags: TagData[] } |
After all tags are cleared via clearTags or setTags (fired before the new tags are added). |
taga11y:change |
{ tags: TagData[] } |
After any mutation, with the final tag set. Fired once per addTags / setTags / delimited paste, after all per-chunk taga11y:add events. |
taga11y:paste |
{ added: TagData[], skipped: string[] } |
Once after a paste gesture that contained a configured single-character delimiter. skipped lists chunks rejected (not in suggestion list in whitelist mode, already selected, or maxTags reached). Not fired for paste without a delimiter. |
taga11y:destroy |
{} |
After destroy() tears down the widget. |
See docs/guides/events.md for full per-event reference and examples.
Full API documentation with all classes, methods, types, and options is generated by TypeDoc and available at /docs/api.
TagData, Taga11yOptions, SuggestionsSource, SuggestionItemtaga11y:add, taga11y:remove, taga11y:clear, taga11y:change, taga11y:paste, taga11y:destroyGuides for contributors verifying and extending taga11y:
See the examples/ directory for runnable demos:
Run the examples locally:
npm run dev
taga11y:add, taga11y:remove, taga11y:clear, taga11y:change, taga11y:paste, taga11y:destroyMIT