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

tab-indexer

v2.0.1

Published

Tab indexer counter with context to make tabIndex settings in a project easy.

Downloads

361

Readme

tab-indexer

npm minzip Coverage License

A tiny context counter for tabIndex. Zero dependencies. ESM + CommonJS.


Setting tabIndex is one of those jobs that looks trivial until you actually do it. Components render in a different order than they appear. Someone adds a field and the whole tab order shifts by one. A modal opens and Tab happily walks you through the sixty inputs behind it. In plain HTML it is annoying; in React it is a part-time job.

I built a system once where every bit of navigation had to work from the keyboard alone — modals opening and closing, focus that had to stay where it belonged. tabIndex needed constant babysitting. So I wrote a counter.

That is all tabIndexer is: a counter, keyed by a string you choose. Ask it for a number, it gives you the next one for that key. Everything else in this README is convenience on top of that one idea.

Install

npm install tab-indexer

The counter

Call it as a template tag, or with a plain string — same thing:

import tabIndexer from "tab-indexer";

tabIndexer`nav`;    // 1
tabIndexer`nav`;    // 2
tabIndexer("nav");  // 3   — string form, handy for a dynamic key
tabIndexer`side`;   // 1   — different key, its own count

In a component you just drop it into tabIndex:

function Header() {
  return (
    <>
      <a href="/"                tabIndex={tabIndexer`header`} />  {/* 1 */}
      <input aria-label="search" tabIndex={tabIndexer`header`} />  {/* 2 */}
      <button                    tabIndex={tabIndexer`header`} />  {/* 3 */}
    </>
  );
}

Start value and step

Two optional arguments: the start value and the step. Pass them once, before you start counting:

tabIndexer`form${100}`;   // sets start to 100, returns 100
tabIndexer`form`;         // 101
tabIndexer`form`;         // 102

tabIndexer`grid${0}${10}`; // start 0, step 10, returns 0
tabIndexer`grid`;          // 10
tabIndexer`grid`;          // 20

I give each region of the app its own key and its own starting number — header at 1, main form at 100, footer at 900. tabIndex values do not have to be contiguous; the browser only cares about the order, so leaving gaps between regions costs nothing and saves you renumbering later.

The third argument used to be called numerator. It was always just the step. The name is fixed; the behaviour is not.

Reading and resetting

tabIndexer.peek("nav");     // current value, WITHOUT advancing. Unknown key → 0
tabIndexer.reset("nav");    // back to the start value (0, or whatever you set)
tabIndexer.reset();         // reset every known key
tabIndexer.contexts();      // { header: 3, form: 102, grid: 20 }  — a snapshot

reset is the one you will actually need. Any time you call tabIndexer from code that runs more than once — a component that re-renders — the counter keeps climbing unless you reset it at the top of each pass. If you forget, you will get a console warning after a thousand advances telling you exactly which key ran away. (Development only. It is not going to nag your users.)

If you use React, @tab-indexer/react does the resetting for you — skip to the bottom.

Isolated instances

The default export is a shared singleton. Every import in your app talks to the same counters, which is usually what you want.

When it is not — a unit test, server-side rendering where one instance per request keeps users from leaking into each other, a self-contained widget — make your own:

import { createTabIndexer } from "tab-indexer";

const tabIndexer = createTabIndexer(); // its own keys, checkers and regions

Named exports: createTabIndexer (the factory) and tabIndexer (the very same singleton as the default export). In CommonJS the default is on .default:

const { tabIndexer, createTabIndexer } = require("tab-indexer");

Regions — the modal problem

Here is the part that used to be hard. A modal opens. Everything behind it still has a tabIndex, so Tab still visits it. A negative tabIndex takes an element out of the order — but now you are the one tracking which elements to flip, and when, and back again. That is the B.S. this utility was supposed to save you from.

So: regions.

tabIndexer.region("modal").activate();

While a region is active, only its key produces real numbers — every other key returns -1. Deactivate it and everything comes back exactly where it was.

tabIndexer`page`;                          // 1
tabIndexer.region("modal").activate();     // modal's counter is reset for you
tabIndexer`modal`;                         // 1
tabIndexer`modal`;                         // 2
tabIndexer`page`;                          // -1   ← gated
tabIndexer.region("modal").deactivate();
tabIndexer`page`;                          // 2    ← carries on

Put activate() where the modal opens, deactivate() where it closes. That is the whole integration.

Nesting

Regions stack. Open a confirmation dialog on top of the modal and the modal is suspended, not cleared — close the dialog and the modal is the active region again:

tabIndexer.region("modal").activate();     // stack: [modal]
tabIndexer.region("confirm").activate();   // stack: [modal, confirm]
tabIndexer`modal`;                         // -1   ← suspended
tabIndexer`confirm`;                       // 1
tabIndexer.region("confirm").deactivate(); // stack: [modal]
tabIndexer`modal`;                         // resumes

Region API

const modal = tabIndexer.region("modal");   // cached handle, string or tag form

modal.activate();                 // → the region (chainable); resets its counter
modal.activate({ reset: false }); // ...unless you say not to
modal.deactivate();               // → the region
modal.active;                     // is it the top of the stack right now?

tabIndexer.activeRegions();        // ["modal", "confirm"]  — oldest first

Regions handle order. They do not trap focus and they do not restore focus when the modal closes — pair them with a focus-trap library for that. Ordering and trapping are two different jobs and this package only claims one of them.

Checkers (the old way, still here)

Before regions there were setChecker / clearChecker — a predicate that runs on every advance and can force a -1. For the modal case, region() replaces it and handles nesting properly, so prefer that. But a per-key checker is still useful when you want to skip or veto individual values within a key:

// skip odd results in "grid"
tabIndexer.setChecker`grid${(currentValue, step) =>
  (currentValue + step) % 2 ? -1 : 1}`;

tabIndexer.clearChecker`grid`;   // remove it

A checker returning 0, a positive number, or true lets the advance through; < 0 or false forces -1. The global form — setChecker(fn) with no key — is superseded by region() and may be deprecated in a future major.

React

@tab-indexer/react wraps all of the above:

import { TabIndexProvider, TabScope, useTabIndex } from "@tab-indexer/react";

function Form() {
  const tab = useTabIndex("form");       // resets itself every render
  return (
    <>
      <input tabIndex={tab()} />          {/* 1 */}
      <input tabIndex={tab()} />          {/* 2 */}
    </>
  );
}

// <TabScope name="modal"> around the dialog activates the region while mounted.
// <TabIndexProvider> at the root ties it all together (and isolates SSR).

API

| | | | --- | --- | | tabIndexer`key` / tabIndexer("key") | advance and return the next index | | tabIndexer`key${start}${step}` | set start and step (returns start) | | tabIndexer.peek(key) | current value, no advance; unknown → 0 | | tabIndexer.reset(key?) | reset one key, or all, to the start value | | tabIndexer.contexts() | { [key]: value } snapshot | | tabIndexer.region(name) | region handle: .activate({reset?}), .deactivate(), .active | | tabIndexer.activeRegions() | active region names, oldest first | | tabIndexer.setChecker / .clearChecker | per-key value veto (legacy) | | createTabIndexer() | a fresh, isolated instance |


Have a good productive day :)

If this saved you an afternoon, you can buy me a coffee.