@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-pluginZoijs 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 updatesThe 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
htmltagged template are inspected. - Only a zero-argument
.get()counts — that's Zoijs's reactive-read shape.map.get(key)andparams.get("id")take an argument and are left alone. - A
.get()reached through a nested function — an event handler, aneach/effect/computedcallback, 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 readAccessibility 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.
| Rule | Flags | Level |
|---|---|---|
alt-text | an <img> with no alt (use alt="" for decorative) | 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 elementThey'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.