a11y-result-count
v1.0.0
Published
Accessible result count and live announcement plugin for filtered or paginated result interfaces.
Maintainers
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-countpnpm add a11y-result-countyarn add a11y-result-countUsage
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 withIntl.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-localedata-a11y-result-count-announce-changesdata-a11y-result-count-announce-delaydata-a11y-result-count-politenessdata-a11y-result-count-assertive-errorsdata-a11y-result-count-formatdata-a11y-result-count-visible-countdata-a11y-result-count-filtered-countdata-a11y-result-count-total-countdata-a11y-result-count-loaded-countdata-a11y-result-count-page-sizedata-a11y-result-count-has-moredata-a11y-result-count-modedata-a11y-result-count-messagefor one initial state messagedata-a11y-result-count-message-onedata-a11y-result-count-message-manydata-a11y-result-count-message-showingdata-a11y-result-count-message-showing-filtereddata-a11y-result-count-message-emptydata-a11y-result-count-message-loadingdata-a11y-result-count-message-errordata-a11y-result-count-message-loaded-moredata-a11y-result-count-message-filtereddata-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:inita11y-result-count:readya11y-result-count:updatea11y-result-count:announcea11y-result-count:loadinga11y-result-count:emptya11y-result-count:errora11y-result-count:reseta11y-result-count:addon-errora11y-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_EVENTSRESULT_COUNT_SELECTORSRESULT_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
assertiveErrorsis 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/.
