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

v1.0.0

Published

Accessible result count and live announcement plugin for filtered or paginated result interfaces.

Readme

A11yResultCount

Accessible result count and live announcement plugin for filtered or paginated result interfaces.

A11yResultCount displays visible result counts and announces meaningful result changes. It does not filter, fetch, paginate, or own result state. Host interfaces pass count state through update().

Installation

npm install a11y-result-count
pnpm add a11y-result-count
yarn add a11y-result-count

Usage

import { createResultCount } from "a11y-result-count";
import "a11y-result-count/styles.css";

const resultCount = createResultCount("[data-a11y-result-count]");

resultCount.update({
  visibleCount: 12,
  filteredCount: 12,
  totalCount: 48,
  loadedCount: 12,
  pageSize: 12,
  hasMore: true,
  mode: "filtered"
});

CSS

Import the default CSS when you want baseline layout, state colors, and visually hidden live-region styling.

import "a11y-result-count/styles.css";

Public custom properties:

  • --a11y-result-count-color
  • --a11y-result-count-muted-color
  • --a11y-result-count-error-color
  • --a11y-result-count-gap

The main block class is .a11y-result-count. State classes are .a11y-result-count--loading, .a11y-result-count--empty, and .a11y-result-count--error. The CSS uses no animation; the reduced-motion rule keeps scroll behavior instant, and forced-colors mode falls back to system colors.

HTML Structure

<p class="a11y-result-count" data-a11y-result-count>
  <span class="a11y-result-count__text" data-a11y-result-count-text>
    Showing 0 of 0 results
  </span>
  <span
    class="a11y-result-count__status"
    data-a11y-result-count-status
    role="status"
    aria-live="polite"
    aria-atomic="true"
  ></span>
  <span
    class="a11y-result-count__status"
    data-a11y-result-count-assertive
    aria-live="assertive"
    aria-atomic="true"
  ></span>
</p>

Use semantic controls in the host interface, then call the instance methods from those controls.

Explicit live regions are recommended for the most predictable DOM shape. Treat them as empty announcement buffers, not as hidden copies of the visible count. The plugin writes announcement text after announceDelay, then clears the announcement-only region so the visible text remains the only persistent result-count content. If the live-region elements are omitted, the plugin generates visually hidden <span> regions. Generated polite regions use role="status" and aria-live="polite"; generated assertive regions use aria-live="assertive" only when assertiveErrors is enabled. When the root itself is the text target, generated live regions are inserted after the root so the visible text target is not polluted.

API

import {
  A11yResultCount,
  RESULT_COUNT_CLASSES,
  RESULT_COUNT_EVENTS,
  RESULT_COUNT_SELECTORS,
  createResultCount,
  initResultCountAll,
  type ResultCountAddon,
  type ResultCountInstance,
  type ResultCountMessages,
  type ResultCountOptions,
  type ResultCountState
} from "a11y-result-count";

Functions

  • createResultCount(root, options?): creates and initializes one instance.
  • initResultCountAll(options?): creates instances for every [data-a11y-result-count] root in the document.

Selector roots must resolve to HTMLElement instances. Missing selectors and selectors that match non-HTMLElement nodes throw a TypeError; initResultCountAll() skips non-HTMLElement matches.

Instance Methods

  • init(): initializes the instance.
  • destroy(): clears timers, generated DOM, state classes, live-region changes, and addon cleanup.
  • update(state): updates count state, visible text, state classes, and optional announcements.
  • announce(message, politeness?): writes a debounced, transient live-region announcement.
  • setLoading(message?): applies loading state.
  • setEmpty(message?): applies empty state.
  • setError(message?): applies error state.
  • reset(): returns to the initial normalized count state, clears timers/live regions, and dispatches reset.
  • setLocale(locale, messages?): updates number formatting and message templates.
  • t(key, params?): formats a configured message.
  • formatNumber(value): formats a number with Intl.NumberFormat.
  • registerAddon(addon), unregisterAddon(name), getAddon(name), getAddons(): manage addon hooks.

Destroyed instances are final. After destroy(), active methods such as update(), announce(), reset(), setLocale(), registerAddon(), and unregisterAddon() throw a clear error. To reinitialize the same root, call createResultCount(root) again.

Options

type ResultCountOptions = {
  locale?: string;
  messages?: Partial<ResultCountMessages>;
  announceChanges?: boolean;
  announceDelay?: number;
  politeness?: "polite" | "assertive";
  assertiveErrors?: boolean;
  format?: "simple" | "showing" | "loaded" | "custom";
  customFormat?: (state, helpers) => string;
  addons?: ResultCountAddon[];
};

Change Default Text

DEFAULT_MESSAGES is internal. To change the text defaults, pass a partial messages object in JavaScript. Override only the keys you need.

const resultCount = createResultCount("[data-a11y-result-count]", {
  messages: {
    showing: "{visibleCount} of {totalCount} resources visible",
    showingFiltered: "{visibleCount} of {filteredCount} resources match",
    empty: "No resources match these filters",
    loading: "Checking resources",
    error: "Resources could not be refreshed"
  }
});

Message templates can use normalized state placeholders such as {count}, {visibleCount}, {filteredCount}, {totalCount}, {loadedCount}, {pageSize}, and {hasMore}. The available message keys are one, many, showing, showingFiltered, empty, loading, error, loadedMore, filtered, and initial.

The same defaults can be set from markup with data-a11y-result-count-message-* attributes. Use the kebab-case form of the message key after message-.

<p
  data-a11y-result-count
  data-a11y-result-count-message-showing="{visibleCount} of {totalCount} resources visible"
  data-a11y-result-count-message-showing-filtered="{visibleCount} of {filteredCount} resources match"
  data-a11y-result-count-message-empty="No resources match these filters"
  data-a11y-result-count-message-loading="Checking resources"
  data-a11y-result-count-message-error="Resources could not be refreshed"
>
  <span data-a11y-result-count-text>Showing 0 of 0 resources</span>
</p>

You can also update locale and templates after initialization:

resultCount.setLocale("en-GB", {
  empty: "No matching resources"
});

Use data-a11y-result-count-message for one initial or custom state message in markup:

<p
  data-a11y-result-count
  data-a11y-result-count-mode="empty"
  data-a11y-result-count-message="No articles match this filter"
>
  <span data-a11y-result-count-text>No articles match this filter</span>
</p>

JavaScript messages override matching data-a11y-result-count-message-* attributes.

Equivalent data-* options are available for simple markup configuration:

  • data-a11y-result-count-locale
  • data-a11y-result-count-announce-changes
  • data-a11y-result-count-announce-delay
  • data-a11y-result-count-politeness
  • data-a11y-result-count-assertive-errors
  • data-a11y-result-count-format
  • data-a11y-result-count-visible-count
  • data-a11y-result-count-filtered-count
  • data-a11y-result-count-total-count
  • data-a11y-result-count-loaded-count
  • data-a11y-result-count-page-size
  • data-a11y-result-count-has-more
  • data-a11y-result-count-mode
  • data-a11y-result-count-message for one initial state message
  • data-a11y-result-count-message-one
  • data-a11y-result-count-message-many
  • data-a11y-result-count-message-showing
  • data-a11y-result-count-message-showing-filtered
  • data-a11y-result-count-message-empty
  • data-a11y-result-count-message-loading
  • data-a11y-result-count-message-error
  • data-a11y-result-count-message-loaded-more
  • data-a11y-result-count-message-filtered
  • data-a11y-result-count-message-initial

JavaScript options override matching data-* options, including message template attributes.

Runtime option and dataset parsing is strict. Boolean options accept booleans or recognized boolean strings such as "true" and "false". Integer values accept finite non-negative integers or digit-only strings after trimming. Partial values such as "12px", decimals, negatives, empty strings, NaN, invalid enum values, non-function customFormat, and non-array addons fall back safely.

State

type ResultCountState = {
  visibleCount?: number;
  filteredCount?: number;
  totalCount?: number;
  loadedCount?: number;
  pageSize?: number;
  hasMore?: boolean;
  mode?: "initial" | "filtered" | "loading" | "empty" | "error";
  message?: string;
  announce?: boolean | string;
  meta?: Record<string, unknown>;
};

Plain JavaScript callers are normalized defensively at runtime even though the TypeScript types stay strict.

Addons

Addons can observe lifecycle hooks and optionally manage setup/cleanup work.

const analyticsAddon: ResultCountAddon = {
  name: "analytics",
  setup(context) {
    const label = context.t("initial", context.state);
    context.root.setAttribute("data-result-count-addon-ready", label);

    return () => {
      context.root.removeAttribute("data-result-count-addon-ready");
    };
  },
  beforeReset(context) {
    context.formatNumber(context.state.totalCount);
  },
  destroy(context) {
    context.t("initial", context.state);
  }
};

registerAddon(addon) requires a non-empty name. Registering a duplicate name cleans up the old addon before installing the replacement. setup(context) runs on registration; a cleanup function returned from setup runs on unregister, replacement, or plugin destroy. Optional addon destroy(context) also runs during unregister, replacement, and plugin destroy. Addon errors are isolated and reported through a11y-result-count:addon-error.

Addon context includes root, instance, normalized state, nullable elements, normalized options, dispatch, t, formatNumber, and announcement metadata when relevant.

Lifecycle Events

Events bubble from the root and include { instance } in event.detail.

  • a11y-result-count:init
  • a11y-result-count:ready
  • a11y-result-count:update
  • a11y-result-count:announce
  • a11y-result-count:loading
  • a11y-result-count:empty
  • a11y-result-count:error
  • a11y-result-count:reset
  • a11y-result-count:addon-error
  • a11y-result-count:destroy

Reset events include { instance, state }. Addon error events include { instance, state, addon, hook, error }.

Selectors

  • [data-a11y-result-count]: root
  • [data-a11y-result-count-text]: visible text target
  • [data-a11y-result-count-status]: polite status region
  • [data-a11y-result-count-polite]: polite live region
  • [data-a11y-result-count-assertive]: assertive live region for errors

Convenience constants are exported for bridge addons and host integrations:

  • RESULT_COUNT_EVENTS
  • RESULT_COUNT_SELECTORS
  • RESULT_COUNT_CLASSES

Accessibility Notes

  • Designed to support visible result-count updates whenever normalized state changes.
  • Designed to support polite live-region announcements for normal updates.
  • Errors use an assertive live region only when assertiveErrors is true.
  • Announcements are debounced to avoid speaking every fast intermediate state.
  • Repeated messages clear the live region before writing the same text again.
  • Announcement-only regions are cleared after they are written so they do not persist as duplicate screen reader content.
  • The plugin does not move focus or add focusable controls.
  • Use real buttons, links, form controls, and labels in the host interface.
  • Test live-region behavior with your target browsers and assistive technologies.
  • This package does not claim complete WCAG conformance by itself; host markup and workflows still need project-specific testing.

Keyboard Behavior

A11yResultCount does not define keyboard shortcuts because it does not add interactive controls. Keyboard behavior belongs to the host filtering, pagination, or load-more controls that call the API.

| Key / action | Plugin behavior | Host responsibility | | --- | --- | --- | | Tab / Shift+Tab | The plugin adds no tabbable elements. | Keep host controls in a logical order. | | Enter / Space | The plugin adds no custom activation handlers. | Use native buttons or links for actions that call the API. | | Escape | No plugin behavior. | Use Escape only for host UI that opens dismissible overlays. | | Arrow keys / Home / End | No plugin behavior. | Use only when the host UI implements a composite widget pattern. |

Focus Behavior

The plugin never moves focus. Loading, empty, error, and update states are communicated through visible text and live regions. Focus should remain on the control that triggered the update unless the host interface has a separate, documented reason to move it.

Screen Reader Behavior

Normal result changes are written to a polite live region after announceDelay. Error states are written to the assertive region only when assertiveErrors is enabled. Announcement-only regions are cleared shortly after they are written to reduce stale or duplicated screen reader browse content. For rapid changes, the pending announcement is replaced so the latest meaningful result state is announced. Automated tests verify DOM updates, live-region text, transient clearing, and focus stability, but actual announcement timing still needs manual screen reader checks.

Privacy And Data Handling

A11yResultCount does not read from storage, write to storage, send network requests, use analytics, write to the clipboard, inspect the page URL, or persist result state. Count state is kept in memory on the instance and is controlled by the host application.

Limitations

  • The plugin does not filter, fetch, paginate, or own result state.
  • Live-region timing can vary by browser and assistive technology.
  • Server-render a visible result count for the best no-JavaScript fallback.
  • Prefer explicit live regions in production markup when possible.
  • Avoid using list or table structural elements as the root when relying on generated live regions.
  • A11yMediaLibrary may coordinate this plugin by calling its methods, but this package does not import A11yMediaLibrary or A11yLoadMore.

Docs Metadata

import { docs } from "a11y-result-count/docs";

Example

See the basic example source. The basic example demonstrates a small resource-list scenario with all, filtered, loading, empty, error, and reset states, using native buttons and visible non-color state text around the plugin output.

GitHub Pages Demo

This repository publishes the enhanced basic demo through GitHub Actions. In the GitHub repository settings, set Pages to use GitHub Actions as the source; the workflow builds dist/, stages examples/basic/index.html as the artifact root, rewrites its local dist/ paths for deployment, and uploads pages-dist/.