@zoijs/eslint-plugin

Lint rules for Zoijs. One small, auto-fixable rule that enforces the framework's single rule — wrap a reactive read in () => — so a stale binding never makes it to the page.

npm install -D eslint @zoijs/eslint-plugin

Zoijs has one 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 while you type and fixes it with eslint --fix.

Setup — flat config (ESLint 9+)#

// eslint.config.js
import zoijs from "@zoijs/eslint-plugin";

export default [
  zoijs.configs.recommended,
];

That enables zoijs/require-reactive-binding as an error.

Setup — legacy config#

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

Reactivity: require-reactive-binding#

Requires a reactive read inside an html template to be wrapped in an arrow function, so the binding updates. Auto-fixable.

// ✗ read once during setup — never updates
html`<h1>${user.get().name}</h1>`
// ✓ live binding — updates when user changes
html`<h1>${() => user.get().name}</h1>`

It's deliberately narrow, so false positives are rare:

  • Only interpolations of an html tagged template are inspected.
  • Only a zero-argument .get() counts — that's 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.
  • .peek() is never flagged. It's 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
This plugin lints your code — it never touches the core. Linting is a build-time concern; the Zoijs core stays runtime-only and dependency-free. See the one rule in action on the Bindings page.

Accessibility rules#

A few high-value checks on the markup inside html templates, reinforcing the accessibility guide. Zoijs writes real HTML, so most of accessibility is using the right element — these catch the common slips.

RuleFlagsLevel
alt-textan <img> with no alt (use alt="" for decorative)error
no-positive-tabindexa literal tabindex greater than 0 (use 0 or -1)error
no-static-element-interactionsan 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

They're deliberately narrow (only literal positive tabindex, only <div>/<span> for the interaction rule, decorative alt="" always allowed) — and not a replacement for an automated auditor or a real screen reader.

Known limitations#

  • Matches the literal tag name html. If you alias or namespace it (lit.html, const h = html), the rules won't recognize those calls.
  • The a11y rules use a small template-markup scanner, not a full HTML parser — they target a few cheap-to-catch mistakes, not full WCAG coverage.