npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

taga11y

v1.1.0

Published

Accessibility-first tagging library

Readme

taga11y

npm version minzipped size zero dependencies license: MIT

Accessible tagging, built in — not bolted on.

A zero-dependency multi-select / tag input with inline chips, built to the WAI-ARIA APG combobox pattern exactly — aria-activedescendant keeps focus on the input throughout dropdown navigation. Keyboard, switch-device, and screen-reader support aren't features here; they're the product.

Out of the box it renders as close to a native form control as it gets, blending into any page untouched, yet it's fully customizable.

~8 KB gzipped  ·  zero dependencies  ·  vanilla JS · React · Vue

WAI-ARIA APG combobox · SR matrix: NVDA · JAWS · VoiceOver · TalkBack · axe-core + Chromium/Firefox/WebKit e2e

➡️ Try the live demos & read the docs ⬅️


Features

  • Accessible by default — full WAI-ARIA APG combobox pattern, screen reader announcements, keyboard and switch device support
  • Progressive enhancement — the original <input> is enhanced in place and works as a form control without JavaScript
  • Three suggestion modes — static arrays, pre-fetched async, and dynamic per-keystroke callbacks
  • Whitelist mode — restrict to predefined suggestions with enforceSuggestions: true
  • Configurable — delimiters, max tags, debounce, theming, custom serializers
  • Dark mode — automatic (prefers-color-scheme) or forced (data-theme)
  • CSS custom properties — retheme from ~6 base tokens; 9 derived tokens cascade automatically (per-token opt-out preserved)
  • Native form integration — hidden input with serialised values, form reset support
  • Zero dependencies — bundles to a single ESM/CJS/IIFE file via Vite

Installation

npm

npm install taga11y

Import the core class and the stylesheet:

import Taga11y from 'taga11y';
import 'taga11y/dist/taga11y.css';

CDN / IIFE

<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>

Also available on jsDelivr, often faster in Europe and Asia:

<script src="https://cdn.jsdelivr.net/npm/taga11y/dist/taga11y.iife.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/taga11y/dist/taga11y.css">

Quick start

<label for="tags">Select tags:</label>
<input id="tags" type="text">
const input = document.querySelector('#tags');
const widget = new Taga11y(input, {
  suggestions: ['JavaScript', 'TypeScript', 'Rust', 'Go', 'Python'],
  maxTags: 10,
  delimiter: [',', 'Enter', 'Tab'],
});

Framework wrappers

First-party React and Vue components are shipped from subpath exports. Both wrap the same vanilla Taga11y class, support controlled and uncontrolled binding, forward every event, expose the underlying instance, and mount client-only (SSR-safe). Import the stylesheet once, as with the core.

Peer dependencies: React 19 (taga11y/react) or Vue 3 (taga11y/vue).

React

import { useState } from 'react';
import { Taga11yInput } from 'taga11y/react';
import 'taga11y/dist/taga11y.css';

function Example() {
  const [tags, setTags] = useState(['JavaScript']);
  return (
    <Taga11yInput
      value={tags}                     // controlled; omit and use defaultValue for uncontrolled
      onChange={(values) => setTags(values)}
      suggestions={['JavaScript', 'TypeScript', 'Rust']}
      maxTags={5}
    />
  );
}

Props: every Taga11yOptions key, plus value/defaultValue (string arrays), the event callbacks onAdd/onRemove/onClear/onChange/onPaste/onDestroy, and instanceRef to receive the live Taga11y instance. A standard ref resolves to the underlying <input> element (use instanceRef for the instance). Unknown props pass through to the underlying <input>.

With react-hook-form

Because onChange is value-first and ref resolves to the <input>, the component drops into a <Controller> via {...field}:

import { useForm, Controller } from 'react-hook-form';
import { Taga11yInput } from 'taga11y/react';

function TagsField() {
  const { control } = useForm({ defaultValues: { tags: [] } });
  return (
    <Controller
      name="tags"
      control={control}
      render={({ field }) => (
        <Taga11yInput {...field} suggestions={['JavaScript', 'TypeScript', 'Rust']} />
      )}
    />
  );
}

field.onChange receives the tag array on every mutation, and field.ref gets the input node so focus-on-validation-error works.

Vue

<script setup>
import { ref } from 'vue';
import { Taga11yInput } from 'taga11y/vue';
import 'taga11y/dist/taga11y.css';

const tags = ref(['JavaScript']);
</script>

<template>
  <Taga11yInput
    v-model="tags"
    :suggestions="['JavaScript', 'TypeScript', 'Rust']"
    :max-tags="5"
  />
</template>

Use v-model (controlled) or defaultValue (uncontrolled). Emits add, remove, clear, change, paste, destroy; the instance is available via a template ref (expose()).

NotesonDestroy/destroy fire during unmount; your framework's own unmount hook is usually the better place for teardown. The i18n option is init-only: change it by remounting, not by updating the prop.

Configuration

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. | | maxSuggestions | number | 10 | Maximum suggestions rendered in the dropdown at once. 0 shows none; to show all matches pass a large number (e.g. 9999999). Negative values are clamped to 0. | | delimiter | string \| string[] | [',', 'Enter', 'Tab'] | Character(s) that commit the current input as a tag. When 'Tab' commits pending text, focus stays on the input; Tab on an empty input moves focus onward. When 'Enter' is omitted, Enter only applies a highlighted suggestion and otherwise passes through (e.g. form submit). | | 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. |

Events

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.

API Docs

Full API documentation with all classes, methods, types, and options is generated by TypeDoc and available at the API reference.

Bundle size

Tiny, zero-dependency, and tree-shakeable. Gzipped:

| Artifact | Gzipped | |---|---| | Core (taga11y.mjs) | ~8.0 KB | | Stylesheet (taga11y.css) | ~1.6 KB | | React wrapper (taga11y/react) | ~1.0 KB + core | | Vue wrapper (taga11y/vue) | ~1.0 KB + core |

The framework wrappers import the core rather than duplicating it, so a React or Vue app pays the wrapper cost once on top of the shared core.

Browser support

Modern evergreen browsers — the 2023+ baseline of Chrome/Edge 120+, Firefox 120+, and Safari 17+. No Internet Explorer.

The baseline is set by the CSS :dir() pseudo-class (used for RTL layout); below it, RTL layout falls back to LTR and every other feature still works. taga11y also relies on AbortController/AbortSignal, Intl.PluralRules, crypto.getRandomValues, and the CSS inset property — all available across this baseline.

The dropdown flips upward when the visible space below the input is too short — decided at open time via the visualViewport API, so soft keyboards and pinch-zoom are accounted for, and working in every supported browser. Where CSS Anchor Positioning (anchor-name, anchor-scope) is available, the open dropdown additionally stays glued to the input on scroll and escapes overflow ancestors; elsewhere it keeps the standard absolute-positioned placement — no functionality is lost.

Guides

Contributing

Guides for contributors verifying and extending taga11y:

Examples

See the examples/ directory for runnable demos:

Run the examples locally:

npm run dev

License

MIT