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

@a11yfred/rogers

v0.2.0

Published

Vanilla-first accessibility debug panel. Always look for the helpers.

Readme

@a11yfred/rogers

Rogers is an accessibility debug tool for testing and development. Drop it into any project to see keyboard focus, accessible names, heading structure, and tab order in real time. No framework required, no dependencies.

Install

npm install --save-dev @a11yfred/rogers

How it works

Rogers has two layers:

  • Core — plain JavaScript functions that watch the DOM. No framework needed.
  • Adapters — thin wrappers for React, Vue, Angular, and Remix that wire the core into your framework's lifecycle.

Each adapter takes your framework's own hooks as parameters. Rogers never imports a framework itself, so it adds nothing to your bundle.

Enabling rogers

Rogers checks globalThis.ROGERS_DEV at startup. If it is not true, all functions return immediately and do nothing.

Set it using a condition your build tool evaluates at build time — not a plain true. This ensures the flag is always false in a production bundle, even if someone forgets to remove the import.

// Vite
if (import.meta.env.DEV) globalThis.ROGERS_DEV = true

// webpack / CRA
if (process.env.NODE_ENV !== 'production') globalThis.ROGERS_DEV = true

// Remix / Next.js
if (process.env.NODE_ENV === 'development') globalThis.ROGERS_DEV = true

Do this once in your app entry point, before rogers is imported. Never write globalThis.ROGERS_DEV = true as a bare statement — a build tool cannot tree-shake a hardcoded true.

For a plain HTML demo or local prototype with no build step, a bare true is fine since there is no production build:

<script>globalThis.ROGERS_DEV = true</script>
<script type="module" src="./your-app.js"></script>

File structure

@a11yfred/rogers
├── core/
│   ├── focus.js      — focus tracking
│   ├── names.js      — accessible name lookup
│   ├── headings.js   — heading collection
│   └── tabstops.js   — tab order
├── overlay/          — DOM overlay renderers (no framework)
├── index.js          — vanilla exports
├── react.js          — React adapter
├── vue.js            — Vue adapter
├── angular.js        — Angular adapter
├── remix3.js         — Remix 3 adapter
└── debug.css         — styles for all overlays

Demo

Run the demo locally with no install required:

npm run demo

Then open http://localhost:3000. The demo shows all four debug tools running on a page with intentional accessibility issues to inspect.

Framework integration

Import debug.css once in your app entry point.

import '@a11yfred/rogers/debug.css'

Vanilla JS

import {
  mountFocusDebugger, mountNamesDebugger,
  mountHeadingMapDebugger, mountTabStopsDebugger,
  mountDebugLauncher,
} from '@a11yfred/rogers'
import '@a11yfred/rogers/debug.css'

const state = { focus: null, names: null, headings: null, tabstops: null }

const launcher = mountDebugLauncher({
  onToggle(key, on) {
    if (on && !state[key])  state[key] = mount(key)
    if (!on && state[key]) { state[key].destroy(); state[key] = null }
  },
})

function mount(key) {
  if (key === 'focus')    return mountFocusDebugger()
  if (key === 'names')    return mountNamesDebugger()
  if (key === 'headings') return mountHeadingMapDebugger()
  if (key === 'tabstops') return mountTabStopsDebugger()
}

React

import { useState, useEffect, useRef } from 'react'
import { createComponents } from '@a11yfred/rogers/react'
import '@a11yfred/rogers/debug.css'

const {
  FocusDebugger, NamesDebugger, HeadingMapDebugger,
  TabStopsDebugger, DebugLauncher,
} = createComponents({ useEffect, useRef })

export default function Root() {
  const [active, setActive] = useState({
    focus: false, names: false, headings: false, tabstops: false,
  })

  function handleToggle(key, on) {
    setActive(prev => ({ ...prev, [key]: on }))
  }

  return (
    <>
      <Outlet />
      <FocusDebugger    enabled={active.focus} />
      <NamesDebugger    enabled={active.names} />
      <HeadingMapDebugger enabled={active.headings} />
      <TabStopsDebugger enabled={active.tabstops} />
      <DebugLauncher    enabled onToggle={handleToggle} />
    </>
  )
}

Vue

import { onMounted, onUnmounted, watch, ref } from 'vue'
import { createComposables } from '@a11yfred/rogers/vue'
import '@a11yfred/rogers/debug.css'

const { useFocusDebugger, useNamesDebugger, useDebugLauncher } =
  createComposables({ onMounted, onUnmounted, watch, ref })

In a component:

<script setup>
import { ref } from 'vue'

const active = ref({ focus: false, names: false, headings: false, tabstops: false })

useFocusDebugger(computed(() => active.value.focus))
useNamesDebugger(computed(() => active.value.names))
useDebugLauncher({
  enabled: ref(true),
  onToggle(key, on) { active.value = { ...active.value, [key]: on } },
})
</script>

Angular

import { inject, DestroyRef } from '@angular/core'
import { createServices } from '@a11yfred/rogers/angular'

const { FocusDebuggerService, NamesDebuggerService, DebugLauncherService } =
  createServices({ DestroyRef, inject })

In a root component:

@Component({
  selector: 'app-root',
  providers: [FocusDebuggerService, NamesDebuggerService, DebugLauncherService],
})
export class AppComponent implements OnInit {
  private focus    = inject(FocusDebuggerService)
  private names    = inject(NamesDebuggerService)
  private launcher = inject(DebugLauncherService)

  ngOnInit() {
    this.launcher.enable({
      onToggle: (key, on) => {
        if (key === 'focus') on ? this.focus.enable() : this.focus.disable()
        if (key === 'names') on ? this.names.enable() : this.names.disable()
      },
    })
  }
}

Remix 3

// app/entry.client.js
import { rogers } from '@a11yfred/rogers/remix3'
import '@a11yfred/rogers/debug.css'

const debug = rogers({
  onToggle(key, on) {
    // handle tool state via your own state management
  },
})

// HMR cleanup
if (import.meta.hot) {
  import.meta.hot.dispose(() => debug.destroy())
}

Vanilla API

Core

| Export | Description | | ------ | ----------- | | createFocusWatcher(onToast) | Watches focus events. Calls onToast({ label, hasFocusOutline, isFocusVisible }) | | createNamesWatcher(onTooltip, onClear) | Watches mouse hover. Calls onTooltip({ name, source, x, y }) | | createHeadingWatcher(onHeadings) | Watches scroll and resize. Calls onHeadings(headings[]) | | createTabStopWatcher(onStop, onClear) | Watches tab keypresses. Calls onStop({ seq, cx, cy, label }) | | formatTarget(el) | Returns a <tag.class> string for an element | | getOutlineInfo(el) | Returns { hasFocusOutline, isFocusVisible } | | flashElement(el) | Briefly highlights the element | | isControl(el) | Returns true if the element is interactive | | getAccessibleName(el) | Returns { name, source } | | collectHeadings() | Returns all headings on the page with metadata | | isTabbable(el) | Returns true if the element is in the tab order | | getTabOrder() | Returns all tabbable elements in order |

Overlays

| Export | Description | | ------ | ----------- | | mountFocusDebugger() | Toast + element flash on keyboard focus | | mountNamesDebugger() | Tooltip showing accessible name on hover | | mountHeadingMapDebugger() | Overlay + panel showing heading structure | | mountTabStopsDebugger() | Numbered overlay showing tab order | | mountDebugLauncher(options) | Floating button with toggle menu | | mountDebugHelp(options) | Full command reference panel | | mountDeployBanner(target) | Fixed banner showing deployment target |

mountDebugLauncher options

| Option | Type | Description | | ------ | ---- | ----------- | | position | string | FAB position. One of bottom-right, bottom-left, bottom-center, top-right, top-left, top-center, middle-right, middle-left. Default: bottom-right | | onToggle | (key, on) => void | Called when a tool is toggled. key is one of focus, names, headings, tabstops | | customTools | Array | Additional tools to show in the menu. Each item: { key, label, desc } | | initialState | Record<string, boolean> | Initial active state per tool key |

Returns { setActive(key, on), destroy() }.

CSS

Import debug.css once. It covers all overlays and is self-contained.

License

MIT


Built with help from Claude.