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

@agencecinq/spinbutton

v1.0.1

Published

Accessible, WAI-ARIA spinbutton as a lightweight Web Component.

Downloads

234

Readme

@agencecinq/spinbutton

Accessible, WAI-ARIA spinbutton as a lightweight Web Component.

A spinbutton restricts its value to a set or range of discrete values. It provides an accessible, keyboard-navigable interface for numerical input that maintains value constraints, supports internationalization through custom text labels, and emits events when values change.

Implementation follows the WAI-ARIA Authoring Practices spinbutton pattern. Inspired by @19h47/spinbutton.

Installation

pnpm add @agencecinq/spinbutton

Usage

Web Component (<cinq-spinbutton>)

<cinq-spinbutton>
  <button data-spinbutton-action="decrease" type="button" aria-label="Decrease" tabindex="-1">
    −
  </button>

  <input
    type="number"
    aria-label="Quantity"
    aria-valuemin="0"
    aria-valuemax="10"
    aria-valuenow="1"
    value="1"
  />

  <button data-spinbutton-action="increase" type="button" aria-label="Increase" tabindex="-1">
    +
  </button>
</cinq-spinbutton>
import "@agencecinq/spinbutton";

Why nothing needs to be initialized

Importing @agencecinq/spinbutton registers the Web Component in the Custom Elements Registry (customElements.define('cinq-spinbutton', Spinbutton)). The browser then automatically upgrades every existing <cinq-spinbutton> in the DOM, calling connectedCallback(), which in turn calls init(). You don't need new Spinbutton(...) or to call init() manually.

Required DOM

| Selector | Required | Role | | ------------------------------------- | -------- | ------------------------------------------------------------------------ | | <cinq-spinbutton> | Yes | Wrapper component, controls the inner <input>. Carries no ARIA state. | | [data-spinbutton-input]input | Yes | The focusable element. Hosts role="spinbutton" and ARIA value state. | | [data-spinbutton-action="increase"] | Optional | Click to increase by step. Auto-disabled at the max. | | [data-spinbutton-action="decrease"] | Optional | Click to decrease by step. Auto-disabled at the min. |

The component appends a visually hidden <div aria-live="polite" aria-atomic="true"> to announce value changes to screen readers. The hiding styles are applied inline so the component stays self-contained — no .sr-only utility class required from the consumer.

ARIA semantics live on the <input>

Per APG, all ARIA state of a spinbutton lives on the focusable element — i.e. your <input>. The component reads aria-valuemin/max/now from it at mount and writes aria-valuenow, aria-valuetext, and aria-invalid to it as the state changes. The host <cinq-spinbutton> carries no ARIA state.

HTML is the source of truth: the component does not auto-set role, auto-migrate attributes, or warn about missing labels. Use a proper a11y linter (axe-core, Lighthouse, eslint-plugin-jsx-a11y) to catch invalid markup.

Practical implications:

  • If you use <input type="number">, the implicit ARIA role is already spinbutton — nothing to add.
  • If you use <input type="text">, set role="spinbutton" explicitly on it.
  • The accessible name must come from aria-label, aria-labelledby, a wrapping <label>, or a <label for="…">. There is no fallback.

Buttons remain out of the tab sequence

The + / buttons must not be in the tab order — they are operated via the keyboard arrows of the input itself (per the APG pattern). Always set tabindex="-1" on them, and provide an aria-label so screen-reader users who do reach them via swipe gestures still get a meaningful name.

Options

Configured via data attributes on the host:

| Attribute | Type | Default | Description | | ------------------------ | ------ | ------- | -------------------------------------------------------------------- | | data-spinbutton-step | number | 1 | Increment used by buttons and arrow keys. | | data-spinbutton-delay | number | 20 | Throttle (ms) before the spinbutton-change event is dispatched. | | data-spinbutton-text | JSON | - | {"single":"item","plural":"items"} — appended to aria-valuetext. |

<cinq-spinbutton
  data-spinbutton-step="5"
  data-spinbutton-text='{"single":"barrel","plural":"barrels"}'
>
  <input
    type="number"
    aria-label="Barrels"
    aria-valuemin="5"
    aria-valuemax="50"
    aria-valuenow="5"
  />
  …
</cinq-spinbutton>

Keyboard support

Strictly the keys defined by the APG pattern. Other keys (Arrow Left/Right, Backspace, Delete, printable characters) are left to the browser so the user can freely edit the input's text.

| Key | Function | | ----------- | ------------------------------------------------- | | Arrow Up | Increase value by step. | | Arrow Down | Decrease value by step. | | Page Up | Increase value by step × 5 (optional per APG). | | Page Down | Decrease value by step × 5 (optional per APG). | | Home | Jump to aria-valuemin (when defined). | | End | Jump to aria-valuemax (when defined). |

Events

| Event | Cancelable | Detail | Description | | ------------------- | ---------- | ------------------- | ------------------------------------ | | spinbutton-change | Yes | { value: number } | Throttled value change notification. |

The event is dispatched on <cinq-spinbutton> and bubbles. The typed value is committed on change (blur / Enter), not on every keystroke, so the user can freely type intermediate values that fall outside the bounds.

const $spinbutton = document.querySelector("cinq-spinbutton");

$spinbutton?.addEventListener("spinbutton-change", (event) => {
  console.log(event.detail.value);
});

Programmatic API

const $spinbutton = document.querySelector("cinq-spinbutton");

$spinbutton.setMin(10);     // Updates min, re-clamps current value
$spinbutton.setMax(200);    // Updates max, re-clamps current value
$spinbutton.setValue(50);   // Sets current value (emits by default)
$spinbutton.increase();     // +step
$spinbutton.decrease();     // -step
$spinbutton.destroy();      // Removes listeners + live region

Build Setup

pnpm -C packages/spinbutton build

Acknowledgments