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

@zoijs/eslint-plugin

v0.3.0

Published

ESLint rules for Zoijs — catch the one reactivity footgun before it ships. Zero dependencies, no build step.

Readme

@zoijs/eslint-plugin

ESLint rules for Zoijs — catch the one reactivity footgun and a few common accessibility mistakes before they ship. Zero dependencies, no build step.

Zoijs has a single rule you have to learn: setup runs once, so wrap a reactive read in an arrow function when it should update the DOM.

${() => count.get()}   // live — updates when count changes
${count.get()}         // static — read once during setup, never updates

The static form is silent: there is no runtime error, the page just sits there with a stale value. This plugin flags it at author time and auto-fixes it.

Install

npm install -D eslint @zoijs/eslint-plugin

Requires ESLint 8 or 9.

Usage — flat config (ESLint 9+, recommended)

In eslint.config.js:

import zoijs from "@zoijs/eslint-plugin";

export default [
  zoijs.configs.recommended,
];

That enables zoijs/require-reactive-binding as an error. To wire it up by hand instead:

import zoijs from "@zoijs/eslint-plugin";

export default [
  {
    plugins: { zoijs },
    rules: { "zoijs/require-reactive-binding": "error" },
  },
];

Usage — legacy config (.eslintrc)

{
  "extends": ["plugin:@zoijs/legacy-recommended"]
}

or rule-by-rule:

{
  "plugins": ["@zoijs"],
  "rules": { "@zoijs/require-reactive-binding": "error" }
}

Rules

require-reactive-binding

Requires a reactive read inside an html template to be wrapped in an arrow function, so the binding updates. Auto-fixableeslint --fix wraps the interpolation in () => … for you.

// ✗ flagged — read once, never updates
html`<h1>${user.get().name}</h1>`

// ✓ fixed — live binding
html`<h1>${() => user.get().name}</h1>`

The rule is deliberately narrow, so false positives are rare:

  • Only interpolations of an html tagged template are inspected.
  • Only a zero-argument .get() counts — that is Zoijs's reactive-read shape. map.get(key) and params.get("id") take an argument and are left alone.
  • A .get() reached through a nested function — an event handler, an each / effect / computed callback, or a ${() => …} binding — is fine. The function defers the read, so it stays reactive; the rule never descends into one.
  • .peek() is never flagged. It is Zoijs's sanctioned non-subscribing read — reach for it when a one-time read is exactly what you mean.
html`<p>${count.peek()}</p>`                       // ✓ intentional one-time read
html`<button onclick=${() => save(count.get())}>` // ✓ deferred in a handler
html`<p>${params.get("id")}</p>`                   // ✓ not a reactive read

The plugin matches the literal tag name html. If you alias or namespace it (lit.html, const h = html), the rule won't recognize those calls.

Accessibility rules

A small set of high-value a11y checks on the markup inside html templates. They reinforce the accessibility guide — Zoijs writes real HTML, so most of accessibility is using the right element; these catch the common slips.

| Rule | Flags | Level | |---|---|---| | alt-text | an <img> with no alt attribute (use alt="" for decorative images) | error | | no-positive-tabindex | a literal tabindex greater than 0 (use 0 or -1) | error | | no-static-element-interactions | an onclick on a <div>/<span> with no role — use a <button> | warn |

html`<img src=${avatar} />`                          // ✗ alt-text: needs alt
html`<img src=${avatar} alt=${name} />`              // ✓
html`<div tabindex="2">…</div>`                      // ✗ no-positive-tabindex
html`<div onclick=${open}>Open</div>`                // ✗ no-static-element-interactions
html`<button onclick=${open}>Open</button>`          // ✓ native element

These are intentionally narrow (only literal positive tabindex, only <div>/<span> for the interaction rule, decorative alt="" always allowed) to keep false positives low. They aren't a replacement for an automated auditor (axe/Lighthouse) or testing with a real screen reader — they catch the mistakes that are cheap to catch at author time.

Security rules

Defense-in-depth on top of the runtime's secure-by-default rendering (inert text, URL scheme allowlisting, blocked event-handler attributes). These catch the two residual footguns the runtime can't sanitize for you. See the security guide.

| Rule | Flags | Level | |---|---|---| | no-target-blank-without-rel | a target="_blank" link with no rel="noopener" (reverse tabnabbing) | error | | no-dynamic-style | a style attribute bound from a dynamic value (CSS injection / data exfiltration) | warn |

html`<a href=${url} target="_blank">Docs</a>`               // ✗ no-target-blank-without-rel
html`<a href=${url} target="_blank" rel="noopener">Docs</a>`// ✓
html`<div style=${cssString}>…</div>`                       // ✗ no-dynamic-style
html`<div class=${() => (active ? "on" : "")}>…</div>`      // ✓ bind a class instead
html`<div style="color:red">…</div>`                        // ✓ static style is fine

Both are narrow: only a static target="_blank" is flagged (a dynamic target/rel is left alone), and no-dynamic-style only fires on an interpolated style value, never a fully static one.

Why a plugin and not a core feature

Linting is a build-time concern; the Zoijs core stays runtime-only and dependency-free. This package lives beside the core and never touches it — it just reads your code and points at the one mistake that has no runtime signal.