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

eslint-plugin-lit-ui-router

v1.2.1

Published

ESLint rules that understand lit-ui-router directives: hrefless anchors, inert hrefs, aria-current conflicts and misplaced directives

Readme

eslint-plugin-lit-ui-router

npm version GitHub Release License: MIT Website

ESLint rules that understand lit-ui-router directives.

A lit-ui-router anchor carries no static href — the element-part directive assigns one at runtime — so stock accessibility rules report every correct call site. These rules keep the base rules' real coverage while understanding what the directives do.

The rest are the directives' own runtime dev warnings, statically: the inert href on a native non-link, the silent aria-current takeover, the aria-current neither the class directive nor the status controller ever writes, and the constructor throw each directive keeps for the part type it does not accept. Each reports at author time, on the whole codebase, in a production build — where the runtime says nothing.

Install

npm install --save-dev eslint-plugin-lit-ui-router

eslint (^9.0.0 || ^10.0.0) is the only peer dependency. anchor-is-valid is vendored from eslint-plugin-lit-a11y, not wrapped around it: lit-a11y is an optional sibling you may also run, never a requirement.

Usage

Flat config (eslint.config.js):

import litUiRouter from 'eslint-plugin-lit-ui-router';

export default [...litUiRouter.configs.recommended];

With lit-a11y alongside, configs.recommended must come after lit-a11y's config: it turns lit-a11y/anchor-is-valid off in favor of ours, which reports the same three messages plus the uiSref carve-out.

import litA11y from 'eslint-plugin-lit-a11y';
import litUiRouter from 'eslint-plugin-lit-ui-router';

export default [
  litA11y.configs.recommended,
  ...litUiRouter.configs.recommended,
];

Without lit-a11y installed, that off line is inert — flat config accepts a severity for an unregistered plugin's rule — so there is nothing to change either way.

Settings

Every rule only looks inside lit templates, and settings.litHtmlSources — lit-a11y's setting, with the same three modes — decides which tagged templates those are:

  • unset (the default): every template tagged html is analysed, whatever html is imported from, plus aliases and namespaces (h, lit.html) imported from lit, lit-html or lit-element.
  • true: only files importing lit, lit-html or lit-element are analysed, and only templates whose tag resolves to that import.
  • an array: as true, with the listed packages added to the accepted sources.
export default [
  ...litUiRouter.configs.recommended,
  { settings: { litHtmlSources: ['@apollo-elements/lit-apollo'] } },
];

Tags resolve through scope, so a parameter or local named html or uiSref never counts as the import. This is stricter than lit-a11y in one place: once a file is gated in, lit-a11y also accepts html aliases and namespaces from any later import, while these rules only accept them from the listed sources — a wrapper module that re-exports html belongs in the array.

settings.linkElements names the tags that are link elements — a design system's <sp-link>, <my-link> — which three rules would otherwise have to guess at:

export default [
  ...litUiRouter.configs.recommended,
  { settings: { linkElements: ['sp-link'] } },
];
  • anchor-is-valid checks a declared element the way it checks <a>: it wants a navigable href, static or assigned by a uiSref element part.
  • sref-assign-href goes quiet on a declared element, the way it does on <a> — it honours the href that the assignHref: true default writes.
  • sref-active-class-aria-current asks a declared element for the aria-current it asks an <a> for.
  • sref-status-aria-current asks the same of a declared element whose classes read a SrefStatusController.

Undeclared tags are unchanged in every rule, so this is purely additive: with no linkElements, each behaves exactly as it did. Each also takes a linkElements option of its own, which replaces the setting for that rule.

settings.allowElementParts says whether an element part counts as the work it does at runtime. @lit-labs/ssr never runs an element part, so a template a server prerenders serves <a ${uiSref('home')}> with no href, and <a ${uiSrefActive(...)}> with neither its active class nor aria-current, until the client hydrates. With false, two rules lint a template as the server renders it:

  • anchor-is-valid stops counting a uiSref element part as the href, and fixes it to href=${srefHref(...)}.
  • sref-active-class-aria-current asks a link carrying a uiSrefActive element part for class=${srefActiveClass(...)} with aria-current=${srefAriaCurrent(...)}.

Unset is true, so with no allowElementParts each behaves exactly as it did. Each also takes an allowElementParts option of its own, which replaces the setting for that rule.

Prerendered templates

The rules cannot tell which templates a server renders, so the setting is scoped with files. A mostly-client app turns it off for its prerendered templates:

export default [
  ...litUiRouter.configs.recommended,
  { files: ['src/prerendered/**'], settings: { allowElementParts: false } },
];

A fully isomorphic app turns it off everywhere and back on for its client-only files:

export default [
  ...litUiRouter.configs.recommended,
  { settings: { allowElementParts: false } },
  { files: ['src/client-only/**'], settings: { allowElementParts: true } },
];

oxlint's overrides take no settings, so in .oxlintrc.json the setting is top-level and an override scopes the rule option, which replaces it:

{
  "settings": { "allowElementParts": false },
  "overrides": [
    {
      "files": ["src/client-only/**"],
      "rules": {
        "lit-ui-router/anchor-is-valid": ["error", { "allowElementParts": true }],
        "lit-ui-router/sref-active-class-aria-current": [
          "error",
          { "allowElementParts": true }
        ]
      }
    }
  ]
}

A mostly-client app leaves the top-level setting out and sets false in the override for its prerendered templates.

oxlint (alpha)

The rules also load into oxlint as JS plugins — every one is syntax-only, with no type information. oxlint does not consume configs.recommended, so list them explicitly in .oxlintrc.json:

{
  "jsPlugins": ["eslint-plugin-lit-ui-router"],
  "rules": {
    "lit-ui-router/anchor-is-valid": "error",
    "lit-ui-router/directive-position": "error",
    "lit-ui-router/sref-active-aria-current": "error",
    "lit-ui-router/sref-active-class-aria-current": "error",
    "lit-ui-router/sref-assign-href": "error",
    "lit-ui-router/sref-status-aria-current": "error"
  }
}

No lit-a11y off line is needed: oxlint ships no lit-a11y rules. eslint is imported type-only, so an oxlint-only host does not need ESLint installed.

oxlint's jsPlugins is alpha and explicitly outside semver. This package's test:oxlint lane gates against the exact pinned oxlint version in its own devDependencies and claims nothing beyond it; a break in a later oxlint surfaces on that bump, not in your lint run.

Running ESLint and oxlint together

ESLint-only, oxlint-only and both-at-once are all supported. If you run both, the recommended split is oxlint owns every lit-ui-router/* rule — it is the fast lane, and one report is better than two — while ESLint keeps only the lit-a11y off.

.oxlintrc.json — the rules run here:

{
  "jsPlugins": ["eslint-plugin-lit-ui-router"],
  "rules": {
    "lit-ui-router/anchor-is-valid": "error",
    "lit-ui-router/directive-position": "error",
    "lit-ui-router/sref-active-aria-current": "error",
    "lit-ui-router/sref-active-class-aria-current": "error",
    "lit-ui-router/sref-assign-href": "error",
    "lit-ui-router/sref-status-aria-current": "error"
  }
}

eslint.config.js — do not spread configs.recommended; turn lit-a11y's rule off by hand:

import litA11y from 'eslint-plugin-lit-a11y';
import oxlint from 'eslint-plugin-oxlint';

export default [
  litA11y.configs.recommended,
  {
    // oxlint owns every lit-ui-router/* rule; this displaces lit-a11y's.
    rules: { 'lit-a11y/anchor-is-valid': 'off' },
  },
  oxlint.buildFromOxlintConfigFile('./.oxlintrc.json'),
];

The manual off is the whole trick: eslint-plugin-oxlint de-duplicates only oxlint's native rule names, and has no handling for jsPlugins rules at all. Spreading configs.recommended on top of the oxlint lane would register every rule a second time, and every finding would report twice.

This repository runs exactly this split — see .oxlintrc.json and eslint.config.ts.

Optionally, exempt test fixtures, whose elements exist to be driven rather than shipped, and whose specs often import uiSref by relative path (which the rule cannot recognise as ours):

{
  "overrides": [
    {
      "files": ["**/*.spec.ts", "**/src/specs/**"],
      "rules": {
        "lit-ui-router/anchor-is-valid": "off",
        "lit-ui-router/sref-assign-href": "off"
      }
    }
  ]
}

Rules

💼 Configurations enabled in.
✅ Set in the recommended configuration.
🔧 Automatically fixable by the --fix CLI option.

| Name                           | Description | 💼 | 🔧 | | :----------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------ | :- | :- | | anchor-is-valid | anchor-is-valid for lit templates, where a uiSref element part counts as its runtime href unless allowElementParts is false, which fixes it to srefHref | ✅ | 🔧 | | directive-position | require each lit-ui-router directive to sit in the template position its part type allows | ✅ | | | sref-active-aria-current | disallow an authored aria-current on an element a uiSrefActive element part manages | ✅ | 🔧 | | sref-active-class-aria-current | require an aria-current binding beside a srefActiveClass binding on a link element, or a uiSrefActive element part when allowElementParts is false | ✅ | 🔧 | | sref-assign-href | require assignHref: 'auto' when a uiSref element part rides a native element with no href | ✅ | 🔧 | | sref-status-aria-current | require an aria-current binding on a link element whose classes read a SrefStatusController | ✅ | 🔧 |

Semver policy

Following ESLint core's own policy: a change that makes recommended or an existing rule stricter — new reports on code that previously passed — ships as a major.

New router surface is the exception. lit-ui-router keeps adding directives, and a rule that can only fire on one of them reports on nothing anybody could already have written — the API it matches did not exist yet. Those rules are additive, so they ship as a minor. The attribute-part tier is the current example: sref-active-class-aria-current, sref-status-aria-current and directive-position's attribute rows match only srefHref, srefActiveClass, srefAriaCurrent and SrefStatusController imported from lit-ui-router, all new in [email protected]. A codebase on an earlier router cannot trip them, which is why they arrive in 1.1 rather than 2.0.

What bounds the exception is the surface a rule can reach, not how rare a report would be in practice. A new report reachable through uiSref or uiSrefActive — shipped since 1.0 — is a major however unlikely it is, as is a tightened option default, and so is a rule for router API that shipped but went undocumented: the line is that the router did not export it, not that nobody used it.

The option-aware tier (sref-assign-href, sref-active-aria-current, directive-position) widened recommended during the 1.0.0 release candidates, where a widening cost nobody a major. The remaining roadmap (a state-aware tier) is the same trajectory, so expect majors, not silent tightening.

Module format and Node support

Published as ESM only. Flat config loads ESM natively; a CommonJS config can require() this package on Node ^20.19.0 or >=22.12.0; the call returns the module namespace, so read the plugin from its .default. The engines range mirrors what the ESLint peer range itself supports, not this repository's own Node version.

The template-analyzer deep import

The rule imports eslint-plugin-lit/lib/template-analyzer.js, a path with no exports-map guarantee, against eslint-plugin-lit@^2.0.0 (a regular dependency, installed for you). That is the same path and range eslint-plugin-lit-a11y itself relies on — a break there breaks lit-a11y first — and this package's test suite runs against the pinned floor versions, so a break surfaces on the bump, not in your lint run.