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

a11y-filter-summary

v1.0.0

Published

Accessible active-filter summary plugin with removable filter chips, clear-all behavior, icon text decoration, and opt-in addons.

Readme

A11yFilterSummary

Accessible active-filter summary plugin with removable filter chips, clear-all behavior, optional form-source integration, and opt-in addons.

A11yFilterSummary can work in two core modes. In manual mode, it renders the filter state your application gives it through update() and only emits remove/clear intent events. In form-source mode, it can read active values from a bound form, generate chips, and update that form when a chip or clear-all button is activated. URL query-state and grouped range summaries are available as separate addons so the main runtime does not read browser history or collapse form fields unless you opt in.

Installation

npm install a11y-filter-summary
pnpm add a11y-filter-summary
yarn add a11y-filter-summary

Usage

Manual State Mode

import {
  createFilterSummary,
  type FilterSummaryItemInput
} from "a11y-filter-summary";
import "a11y-filter-summary/styles.css";

const root = document.querySelector<HTMLElement>("[data-a11y-filter-summary]");

if (root) {
  const summary = createFilterSummary(root);
  let filters: FilterSummaryItemInput[] = [
    { name: "type", value: "Image", groupLabel: "Type" }
  ];

  root.addEventListener("a11y-filter-summary:remove", (event) => {
    const { name, value } = event.detail;
    filters = filters.filter(
      (filter) => filter.name !== name || filter.value !== value
    );
    summary.update(filters);
  });

  root.addEventListener("a11y-filter-summary:clear", () => {
    filters = [];
    summary.update(filters);
  });

  summary.update(filters);
}

Form Source Mode

Use form mode when you want the plug-and-play path: provide a normal HTML form and let the plugin derive the active filter summary from labeled controls.

import { createFormFilterSummary } from "a11y-filter-summary";
import "a11y-filter-summary/styles.css";

const root = document.querySelector<HTMLElement>("[data-a11y-filter-summary]");
const form = document.querySelector<HTMLFormElement>("#media-filters");

if (root && form) {
  const summary = createFormFilterSummary(root, form, {
    updateOn: "input",
    ignoredDefaults: {
      sort: "relevance"
    }
  });

  summary.update();
}

Equivalent setup through the main factory:

createFilterSummary(root, {
  source: "form",
  form: "#media-filters"
});

URL State Addon

Use the URL-state addon when active filters should come from query parameters and chip removal or clear-all should update the address bar.

import { createFilterSummary } from "a11y-filter-summary";
import { createUrlStateAddon } from "a11y-filter-summary/addons/url-state";
import "a11y-filter-summary/styles.css";

const root = document.querySelector<HTMLElement>("[data-a11y-filter-summary]");

if (root) {
  createFilterSummary(root, {
    addons: [
      createUrlStateAddon({
        filterParams: ["category", "brand", "size", "sort", "page"],
        ignoredDefaults: {
          sort: "relevance",
          page: "1"
        },
        labels: {
          category: "Category",
          brand: "Brand",
          size: "Size"
        }
      })
    ]
  });
}

The addon is not imported by the main package entry. It reads window.location.search on init and popstate, renders repeated params as separate chips, and updates the URL with pushState by default when a chip or clear-all button is activated. Set historyMode: "replace" to use replaceState.

URL state is visible in the address bar, browser history, shared links, referrers, and potentially server logs. Do not use the addon for sensitive filters or personal data.

Range Groups Addon

Use the range-groups addon when paired form controls should appear as one readable active-filter chip, such as minimum and maximum price or start and end dates.

import { createFormFilterSummary } from "a11y-filter-summary";
import { createRangeGroupsAddon } from "a11y-filter-summary/addons/range-groups";
import "a11y-filter-summary/styles.css";

const root = document.querySelector<HTMLElement>("[data-a11y-filter-summary]");
const form = document.querySelector<HTMLFormElement>("#catalog-filters");

if (root && form) {
  createFormFilterSummary(root, form, {
    labels: {
      minRating: "Rating"
    },
    formatFormValue: ({ name, value }) =>
      name === "minRating" ? `${value} stars and up` : undefined,
    addons: [
      createRangeGroupsAddon({
        form,
        groups: [
          {
            key: "price",
            label: "Price",
            fields: ["minPrice", "maxPrice"],
            formatter: ([min, max]) => {
              if (min && max) return `€${min}-€${max}`;
              if (min) return `€${min} and up`;
              if (max) return `Up to €${max}`;
              return "";
            }
          },
          {
            key: "date",
            label: "Date",
            fields: ["startDate", "endDate"],
            formatter: ([start, end]) => {
              if (start && end) return `${start} to ${end}`;
              if (start) return `${start} and later`;
              if (end) return `Until ${end}`;
              return "";
            }
          }
        ]
      })
    ]
  });
}

The addon is not imported by the main package entry. It transforms normalized form filters before render, keeps unrelated filters as separate chips, and clears every configured source control when a grouped chip is removed. If no form is provided and no ancestor form can be resolved, grouped chips can still render, but chip removal remains host-owned.

Icon Text Addon

Use the icon-text addon when chip labels should include decorative icons for faster scanning while keeping the text label as the source of truth.

import { createFilterSummary } from "a11y-filter-summary";
import { createIconTextAddon } from "a11y-filter-summary/addons/icon-text";
import "a11y-filter-summary/styles.css";

const root = document.querySelector("[data-a11y-filter-summary]");

const summary = createFilterSummary(root, {
  addons: [
    createIconTextAddon({
      // Use icons for stable mappings by filter.name.
      icons: {
        assetType: "image",
        license: "shield"
      },

      // Use resolveIcon when the icon depends on the current filter value.
      resolveIcon: ({ filter }) => {
        if (filter.name !== "resultState") return undefined;

        switch (String(filter.value).toLowerCase()) {
          case "syncing":
            return "sync";
          case "synced":
            return "check";
          case "sync failed":
          case "error":
            return "error";
          case "needs review":
          case "warning":
            return "alert";
          case "no matches":
          case "empty":
            return "empty";
          default:
            return "clock";
        }
      }
    })
  ]
});

summary.update([
  { name: "assetType", value: "Image", groupLabel: "Type" },
  { name: "license", value: "Creative Commons", groupLabel: "License" },
  { name: "resultState", value: "Syncing", groupLabel: "State" }
]);

The rendered chips keep their full text, such as State: Syncing; the addon only prepends decorative SVG icons with aria-hidden="true". It does not replace label text, remove button names, event detail, or live-region messages.

CSS

Import the default CSS when you want baseline layout, chip styling, visible focus states, disabled state styling, and polite live-region hiding.

import "a11y-filter-summary/styles.css";

Public custom properties:

  • --a11y-filter-summary-gap
  • --a11y-filter-summary-chip-background
  • --a11y-filter-summary-chip-color
  • --a11y-filter-summary-chip-border
  • --a11y-filter-summary-focus-ring

HTML Structure

Start with a semantic section or region. The plugin creates a visible heading, list, clear-all button, empty message, and polite status region when renderMode is "replace".

<section data-a11y-filter-summary></section>

For form-source mode, bind the summary to a form in JavaScript or with data attributes:

<form id="media-filters">
  <section
    data-a11y-filter-summary
    data-a11y-filter-summary-source="form"
    data-a11y-filter-summary-form="#media-filters"
  ></section>
</form>

Add data-a11y-filter-summary-ignore to a control that should never become a visible chip.

If you use renderMode: "sync-existing", the plugin reuses matching elements where possible:

<section data-a11y-filter-summary>
  <h2 data-a11y-filter-summary-heading>Active filters</h2>
  <ul data-a11y-filter-summary-list></ul>
  <p data-a11y-filter-summary-empty>No active filters.</p>
  <button type="button" data-a11y-filter-summary-clear>Clear all filters</button>
  <div data-a11y-filter-summary-status role="status" aria-live="polite" aria-atomic="true"></div>
</section>

API

import {
  A11yFilterSummary,
  createFormFilterSummary,
  createFilterSummary,
  initFilterSummaryAll
} from "a11y-filter-summary";

import { createRangeGroupsAddon } from "a11y-filter-summary/addons/range-groups";
import { createUrlStateAddon } from "a11y-filter-summary/addons/url-state";
import { createIconTextAddon } from "a11y-filter-summary/addons/icon-text";
  • new A11yFilterSummary(root, options?): creates an instance. Duplicate construction for the same root returns the existing instance.
  • createFilterSummary(root, options?): creates and initializes one instance.
  • createFormFilterSummary(root, form, options?): creates and initializes one form-source instance.
  • initFilterSummaryAll(options?): initializes all [data-a11y-filter-summary] roots.
  • update(filters?, context?): renders active filters from an array or object map. In form-source mode, calling without filters rereads the bound form.
  • clear(): emits clear intent in manual mode. In form-source mode, resets or clears the bound form and rerenders.
  • setEmptyState(message?): updates the empty-state message.
  • enable() / disable(): toggles action availability.
  • setLocale(locale, messages?): switches to a locale identifier or locale catalog and rerenders generated text.
  • t(key, params?): formats a localized message.
  • registerAddon(addon) / unregisterAddon(name): manages optional hooks.
  • getAddon(name) / getAddons(): reads registered addons.
  • destroy(): removes listeners, generated DOM, state classes, and the duplicate-init cache entry.
  • createUrlStateAddon(options?): creates the optional URL-state addon from a11y-filter-summary/addons/url-state.
  • createRangeGroupsAddon(options): creates the optional grouped range addon from a11y-filter-summary/addons/range-groups.
  • createIconTextAddon(options?): creates the optional icon text addon from a11y-filter-summary/addons/icon-text.

Options

interface FilterSummaryOptions {
  locale?: string | FilterSummaryLocale;
  messages?: Partial<FilterSummaryMessages>;
  source?: "manual" | "form";
  form?: HTMLFormElement | string;
  labels?: Record<string, string>;
  ignoredDefaults?: Record<string, unknown | unknown[]>;
  clearMode?: "reset" | "empty";
  updateOn?: "change" | "input";
  formatFormValue?: (context: FilterSummaryFormValueContext) => string | undefined;
  renderMode?: "replace" | "sync-existing";
  showWhenEmpty?: boolean;
  visuallyHideGroupLabels?: boolean;
  clearAll?: boolean;
  disabled?: boolean;
  restoreFocusAfterRemove?: boolean;
  focusStrategy?: "next" | "previous" | "clear" | "root" | "none";
  headingLevel?: 2 | 3 | 4 | 5 | 6;
  announceUpdates?: boolean;
  announcementClearDelay?: number;
  formatFilterLabel?: (filter: FilterSummaryItem) => string;
  addons?: FilterSummaryAddon[];
}

Generated headings use headingLevel: 2 by default. Set visuallyHideGroupLabels: true to show chip values while keeping group labels available to assistive technologies, for example Image visually with Type: Image as the text alternative. Set clearMode: "empty" in form-source mode when clear-all should empty controls instead of using the form's native reset behavior. Set announceUpdates: false when another component owns filter or result announcements. announcementClearDelay controls how long transient status messages remain in the live region; the default is 1500 milliseconds.

Root data attributes can configure string, number, and boolean options for no-build markup. Use JavaScript options for messages, labels, ignoredDefaults, formatFormValue, formatFilterLabel, and addons.

Localization

English is built into the core. German, Spanish, Italian, French, and Greek are available as opt-in entry points, so applications only bundle the catalogs they import:

import { createFilterSummary } from "a11y-filter-summary";
import { el } from "a11y-filter-summary/locales/el";

const summary = createFilterSummary(document.querySelector("[data-a11y-filter-summary]"), {
  locale: el
});

// Runtime language switching updates generated UI text and accessible names.
summary.setLocale(el);

Available catalog imports are locales/de, locales/es, locales/it, locales/fr, and locales/el. Catalogs use Intl.PluralRules for active-filter count announcements. Message values may be strings with placeholders or formatter functions:

createFilterSummary(root, {
  locale: "el",
  messages: {
    clearAll: "Καθαρισμός όλων των φίλτρων",
    removeFilterWithGroup: ({ group, value }, { locale }) =>
      `${locale}: ${group} — ${value}`
  }
});

The plugin translates only text it owns. Translate filter names and values through localized form labels and options, the labels option, explicit groupLabel/label values, formatFormValue, or formatFilterLabel. Object-map keys are machine identifiers and humanize() cannot translate them, so localized interfaces should not rely on inferred group labels.

Set the correct lang and, for right-to-left languages, dir on the document or containing element. The plugin does not change either attribute. The icon-text addon's inference recognizes English-like machine tokens; icons are decorative, and localized applications can provide explicit icons or resolveIcon mappings.

Input

Array input:

summary.update([
  { name: "type", value: "Image", groupLabel: "Type" },
  { name: "license", value: "Creative Commons", groupLabel: "License" }
]);

Object-map input:

summary.update({
  type: "Image",
  license: ["Creative Commons", "Editorial"]
});

The plugin normalizes each active filter to name, value, label, groupLabel, removeLabel, optional count, and optional meta.

Form-source mode reads text/search/number/date-like inputs, textareas, selects, checkboxes, and radios. It ignores disabled controls, buttons, submit/reset controls, file inputs, password inputs, hidden inputs, empty values, and controls marked with data-a11y-filter-summary-ignore. Labels are derived from labels, fieldset legends, explicit labels, wrapping labels, then a humanized control name.

URL-state addon mode reads query params from window.location.search. If filterParams is provided, only those params are summarized and their order is preserved. If it is omitted, all query keys are considered except empty values and configured ignoredDefaults. Repeated params become separate chips, and formatValue can customize visible chip text without changing the URL value used for removal.

Range-groups addon mode works with form-source summaries. It receives the normalized form filters, replaces configured field sets with one synthetic chip when the formatter returns text, and leaves unrelated filters unchanged. Removing a grouped chip clears each configured source control and rerenders the form summary.

Events

All public plugin events bubble and include { instance } in detail.

  • a11y-filter-summary:init
  • a11y-filter-summary:ready
  • a11y-filter-summary:update
  • a11y-filter-summary:remove
  • a11y-filter-summary:clear
  • a11y-filter-summary:empty
  • a11y-filter-summary:error
  • a11y-filter-summary:addon-error
  • a11y-filter-summary:destroy

Update detail includes filters and optional context. Remove detail includes name, value, label, item, and trigger. Clear detail includes trigger when a clear button caused the event. Error and addon-error details include the thrown error; addon-error details also include the addon name and hook.

Selectors

  • Root: [data-a11y-filter-summary]
  • Heading: [data-a11y-filter-summary-heading]
  • List: [data-a11y-filter-summary-list]
  • Item: [data-a11y-filter-summary-item]
  • Remove: [data-a11y-filter-summary-remove]
  • Clear: [data-a11y-filter-summary-clear]
  • Empty: [data-a11y-filter-summary-empty]
  • Status: [data-a11y-filter-summary-status]
  • Form source: data-a11y-filter-summary-source="form"
  • Form selector: data-a11y-filter-summary-form="#filters"
  • Render mode: data-a11y-filter-summary-render-mode="sync-existing"
  • Clear mode: data-a11y-filter-summary-clear-mode="empty"
  • Update trigger: data-a11y-filter-summary-update-on="input"
  • Show when empty: data-a11y-filter-summary-show-when-empty="false"
  • Clear all: data-a11y-filter-summary-clear-all="false"
  • Disabled: data-a11y-filter-summary-disabled="true"
  • Restore focus: data-a11y-filter-summary-restore-focus-after-remove="false"
  • Focus strategy: data-a11y-filter-summary-focus-strategy="previous"
  • Heading level: data-a11y-filter-summary-heading-level="3"
  • Locale: data-a11y-filter-summary-locale="en"
  • Visually hidden group labels: data-a11y-filter-summary-visually-hide-group-labels="true"
  • Announcements: data-a11y-filter-summary-announce-updates="false"
  • Announcement clear delay: data-a11y-filter-summary-announcement-clear-delay="1500"
  • Ignored form control: [data-a11y-filter-summary-ignore]

Boolean data attributes accept empty string, true, 1, yes, or on as true values and false, 0, no, or off as false values.

Accessibility Notes

  • Uses a semantic list for active filters and real button type="button" controls for removal and clear-all.
  • Each remove button receives a descriptive accessible name, such as Remove Type: Image.
  • Does not use aria-pressed, because removal is a momentary action.
  • Does not steal focus during ordinary update() calls.
  • After the host removes a chip in response to a remove event, focus can move predictably based on focusStrategy.
  • After clear-all removes the focused clear button, focus moves to a safe fallback when focus was already inside the summary.
  • Uses a role="status" polite, atomic live region for short transient status updates and avoids result-count announcements.
  • Enabled summaries do not carry aria-disabled; disabled summaries add aria-disabled="true" on the root and native disabled on buttons.
  • Form-source mode only summarizes values that are already present in the bound form. Do not bind sensitive controls whose values should not become visible.

The included example demonstrates accessible structure and keyboard-friendly controls. Test with your target browsers and assistive technologies before shipping in a product.

Keyboard Behavior

  • Tab reaches each remove button and the clear-all button in DOM order.
  • Enter and Space activate focused buttons through native button behavior.
  • Focus restoration after removal can target the next remove button, previous remove button, clear-all button, the root region, or no forced target.
  • Focus restoration after clear-all prefers a still-visible clear button, then the root region.

Addons

Addons can observe lifecycle hooks or transform normalized filters before render. Hook failures are isolated and dispatch a11y-filter-summary:addon-error.

summary.registerAddon({
  name: "analytics",
  afterRemove({ item }) {
    console.log("remove requested", item.name, item.value);
  }
});

Supported hooks:

  • beforeInit
  • afterInit
  • beforeUpdate
  • afterUpdate
  • transformFilters
  • beforeRender
  • afterRender
  • beforeRemove
  • afterRemove
  • beforeClear
  • afterClear
  • beforeDestroy
  • afterDestroy
  • onError

transformFilters receives normalized FilterSummaryItem[] and may return a replacement array. Return undefined to keep the current filter set.

URL-state addon options

interface UrlStateAddonOptions {
  filterParams?: string[];
  ignoredDefaults?: Record<string, string | string[]>;
  labels?: Record<string, string>;
  historyMode?: "push" | "replace";
  formatValue?: (context: {
    name: string;
    value: string;
    params: URLSearchParams;
  }) => string | undefined;
}

Range-groups addon options

interface RangeGroupsAddonOptions {
  form?: HTMLFormElement | string;
  groups: Array<{
    key: string;
    label: string;
    fields: string[];
    formatter: (values: unknown[], context: RangeGroupFormatterContext) => string | undefined;
    removeLabel?: string | ((context: RangeGroupFormatterContext & {
      displayValue: string;
      labelText: string;
    }) => string);
  }>;
}

Icon-text addon options

type IconTextIconName =
  | "alert"
  | "calendar"
  | "check"
  | "clock"
  | "crop"
  | "empty"
  | "error"
  | "file"
  | "filter"
  | "image"
  | "palette"
  | "search"
  | "shield"
  | "star"
  | "sync"
  | "tag"
  | "video"
  | "wallet";

interface IconTextAddonOptions {
  icons?: Partial<Record<string, IconTextIconName>>;
  resolveIcon?: (context: {
    filter: FilterSummaryItem;
    index: number;
    root: HTMLElement;
  }) => IconTextIconName | false | undefined;
}

Limitations

A11yFilterSummary intentionally does not filter content, own application result state, group range fields by default, choose product-specific icon semantics, manage mobile filter drawers, or announce result counts. Pair it with your application state, search/filter logic, and any separate result-count component. Form-source mode mutates the bound form only for chip removal and clear-all behavior. URL parameters are read and written only when the separate URL-state addon is imported and registered. Range fields are collapsed only when the separate range-groups addon is imported and registered. Chip icons are added only when the separate icon-text addon is imported and registered.

Examples

GitHub Pages

This repository is prepared for GitHub Pages branch publishing from /docs.

  • npm run pages:build builds package artifacts into root /dist and regenerates the committed /docs publish folder.
  • npm run build runs the same Pages build for local verification.
  • npm run build:dist builds package artifacts only. prepack uses this package-only build so npm publishing does not depend on the Pages folder.

The generated /docs folder contains index.html, examples/, demo.css, .nojekyll, and dist/ files copied from the package build. In GitHub, set Settings -> Pages -> Build and deployment -> Source to Deploy from a branch, then select the publishing branch and /docs.

Docs Metadata

import { docs } from "a11y-filter-summary/docs";