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

html-validate-ember

v0.7.4

Published

html-validate transformer for Ember .gts (Glimmer) templates

Readme

HTML-validate for your Ember templates (.gts, .gjs, .hbs): Run HTML5 spec checks, accessibility rules, content-model rules, and form-correctness rules — with diagnostics pointing at exact source positions.

Install

html-validate is a peer dependency, so install both:

pnpm add --save-dev html-validate html-validate-ember

Configure

Create .htmlvalidate.json at your project root:

{
  "extends": ["html-validate:recommended", "html-validate-ember:gts-recommended"],
  "plugins": ["html-validate-ember"],
  "transform": {
    "^.*\\.(gts|gjs|hbs)$": "html-validate-ember"
  }
}

Two presets

Pick whichever fits your project:

  • html-validate-ember:gts-recommended (recommended for most projects) — everything in :recommended plus Ember/Glimmer style conventions baked in (void-style: selfclosing to match ember-template-lint's self-closing-void-elements, etc.).
  • html-validate-ember:recommended (minimal) — only the rule disables that are required for the transformer to behave correctly: no-trailing-whitespace, no-self-closing, attr-quotes, no-raw-characters. No stylistic opinions.

Run

The recommended pattern is to wire the bundled validate-gts into package.json scripts:

"scripts": {
  "lint:html": "validate-gts app/templates app/components"
}

Then:

pnpm lint:html

Wire it into CI alongside your existing lint scripts; non-zero exit fails the build.

For ad-hoc one-off runs, use pnpm exec:

pnpm exec validate-gts app/components/foo.gts            # single file
pnpm exec validate-gts --no-glint app/components/foo.gts # single file no glint
pnpm exec validate-gts app/templates                     # directory (walked recursively)

Progressive enhancement

  1. Bare (no project config): bundled :gts-recommended preset, built-in components (<Input> / <Textarea> / <LinkTo>) substitute to native tags, other components blank transparently as a fallback, static-text resolution via t-helper / if-helper / top-level consts.
  2. + project .htmlvalidate.json: your rules / extends / transform overrides apply. Bundled CLI loads + merges them.
  3. + install @glint/ember-tsc (auto-detected, default on; pass --no-glint to opt out): Signature['Element'] resolves component invocations to native tags. Type-narrowed @arg values flow into attribute enum checks. Splatted-root literal attributes propagate (e.g. <MySlider /> substitutes to <input type='range' min='0' max='100' /> with the actual literal values from the imported component's template).

What it catches

html-validate's recommended preset enables 80 rules covering HTML5 content models, ARIA / WCAG, form correctness, and attribute validity.

A few examples — real bugs that ember-template-lint and eslint-plugin-ember don't catch:

Block element inside a <p> silently closes the paragraph

<p class='text-sm text-gray-600'>
  Some explanation
  <button>?</button>       
  <div popover>...</div>   {{!-- ← parser auto-closes <p> before the <div> --}}
</p>                       {{!-- ← stray </p>, doesn't match anything --}}
templates/page.gts:42: error [no-implicit-close] Element <p> is implicitly closed by parent </div>
templates/page.gts:74: error [close-order] Stray end tag '</p>'

The browser silently rewrites the DOM; your screen-reader tree doesn't match what you wrote.

Duplicate IDs (cross-component when @glint/ember-tsc is installed)

<input type='number' name='price' id='price' value='1000' />
<input type='number' name='price' id='price' value='2000' />
templates/pricing-table.gts:128: error [no-dup-id] Duplicate ID "price"
templates/pricing-table.gts:128: error [form-dup-name] Duplicate form control name "price"

Invalid attribute values

<img alt='Logo' width='100px' />   {{!-- ← width must be unitless integer --}}
<input type='checkbox' readonly /> {{!-- ← readonly only valid on text-like inputs --}}
components/footer.gts:12: error [attribute-allowed-values] Attribute "width" has invalid value "100px"
templates/admin.gts:18: error [input-attributes] Attribute "readonly" not allowed on <input type="checkbox">

How this fits in the Ember linting stack

| Tool | Question it answers | | --- | --- | | eslint-plugin-ember / ember-template-lint | Is this idiomatic Ember? Invocation style, reactivity, modifier API, built-in components, plus JS/TS-side rules from eslint-plugin-ember and some overlap of HTML spec and ARIA rules | | html-validate-ember (this plugin) | Is the rendered HTML spec-correct and accessible? — content model, ARIA / WCAG, form correctness, attribute validity, duplicate IDs, unique landmarks. |


Inline errors in VS Code

Install the official extension: html-validate.vscode-html-validate.

Add the Ember/Glimmer language IDs to your project's .vscode/settings.json:

{
  "html-validate.validate": [
    "html",
    "javascript",
    "markdown",
    "vue",
    "vue-html",
    "glimmer-ts",
    "glimmer-js",
    "handlebars"
  ]
}

Glint integration

Install @glint/ember-tsc in your project and the transformer extracts TypeScript type information for two patterns. Default on when installed; pass --no-glint (or HVE_GLINT=0) to opt out.

1. String-literal-union narrowing in attribute positions

interface PopoverSig {
  Args: { mode: 'auto' | 'manual' | 'hint' }
}

class Popover extends Component<PopoverSig> {
  <template>
    <div popover={{@mode}}>...</div>
  </template>
}

Without Glint: <div popover=""> (DynamicValue, no enum check). With Glint: html-validate sees popover="auto" (or whichever union member is not in html-validate's enum, surfacing a typing bug if you've declared an invalid value).

2. Component → element substitution

When a component declares Signature['Element'], the transformer substitutes the invocation with the corresponding native tag, so content-model rules apply correctly:

class MyButton extends Component<{
  Element: HTMLButtonElement
  Args: { onClick: () => void }
}> { /* ... */ }
<MyButton @onClick={{this.foo}} />

→ html-validate sees a <button>, can apply no-implicit-button-type / text-content rules accordingly. (The transformer adds DynamicValue placeholders for type and label content so those rules don't FP-fire on substituted self-closing components — the actual button has its type and label set internally.)

For components with Element: unknown (typically yield-only components) the transformer treats the invocation as transparent — children float into the parent's content model.

Disabling Glint

Glint adds per-file overhead (TS program build, module rewrite, TypeChecker calls). The cache makes repeat runs over unchanged files cheap, but the file you're actively editing pays the cost each iteration. Pass --no-glint (or HVE_GLINT=0) to skip — you keep built-in component substitution but lose Signature['Element'] resolution and @arg enum narrowing.

Caching

Glint results are content-addressed and cached on disk under node_modules/.cache/html-validate-ember/glint/. Set HVE_NO_CACHE=1 to bypass the cache.

Silencing rules

Three layers, broadest to narrowest:

  1. Project config — disable any rule in .htmlvalidate.json:

    {
      "rules": {
        "aria-label-misuse": ["error", { "allowAnyNamable": true }],
        "no-inline-style": "off"
      }
    }
  2. File-level disable — html-validate directives at the top of a file work normally:

    {{!-- [html-validate-disable no-dup-id] --}}
  3. Per-element directive — Glimmer comment containing the directive:

    {{!-- [html-validate-disable-next no-dup-id] --}}
    <div id={{this.id}}>x</div>
    
    {{! [html-validate-disable-next no-dup-id] }}
    <div id={{this.id}}>x</div>

    Inline reason / link — append -- text inside the brackets (html-validate's directive parser splits on -- after the rule name):

    {{!-- [html-validate-disable-next unique-landmark -- pending https://github.com/w3c/html-aria/issues/579] --}}
    <header>...</header>

    Variants (per html-validate's inline-config docs):

content-tag gives byte offsets for each <template> block's content. We compute line:column for the block's start, attach as the Source base, and emit length-equivalent HTML so byte positions inside the template match positions inside the original .gts. html-validate adds reported positions to the base. No SourceMap machinery — same approach html-validate-vue and html-validate-angular use.

{{#if}}/{{else}} (and {{else if}} chains) are validated per branch by default. The transformer enumerates branch combinations, yields one html-validate Source per combination, and html-validate validates each independently. Errors from every branch surface — including the un-selected branch under single-pass.

Enumeration is capped at the first 10 conditional branches per template to bound work; "conditional branch" here means any block helper with both a program and an {{else}} clause. The common forms are {{#if/else}}, {{#unless/else}}, and {{#each/else}} (empty fallback), but custom block helpers ({{#my-helper x}}A{{else}}B{{/my-helper}}) count too. Surplus conditional branches fall back to the single-branch heuristic, which can hide errors in their unselected arms. Override with --max-conditional-branches=N on validate-gts, or HVE_MAX_CONDITIONAL_BRANCHES=N when invoking html-validate directly. Set N=0 to disable multipass and use the single-branch heuristic everywhere.

Enumeration is tree-aware: branches are organized by nesting, and choosing one arm of a branch only enumerates that arm's nested branches. This matches the runtime DOM — choices inside a blanked arm can't affect what html-validate sees — and turns 2^N worst case into far fewer calls on nested templates. A 6-deep {{#if}}/{{else}} chain produces 7 distinct passes rather than 64. Pure-sibling branches still scale 2^N, so the cap matters most there.

The bundled validate-gts CLI dedupes identical messages by (line, column, ruleId, message) before printing, so an error stable across branches (e.g., a misnested element outside the if/else) is reported once even though it lives in every pass. The dedupe util is also exported as dedupeMultipassReport from lib/multipass-dedupe.js for custom consumers.

  • Static-string scope. {{NAME}} resolves against same-file const NAME = '...' declarations and one-level-deep import { NAME } from './sibling' (relative paths only — package and path-aliased imports are skipped). {{this.field}} resolves against same-file class-field initializers (field = '...' or field: T = '...'). What's not resolved: transitive re-exports (export { X } from './...' chains), default imports, namespace imports, and getters returning literals — Glint narrows some of these to string-literal types when @glint/ember-tsc is installed, which the blanker picks up through a separate code path.
  • no-implicit-button-type fires on every untyped <button> regardless of <form> ancestry — that's html-validate's strict design (default type=submit is non-obvious). The plugin doesn't try to soften it; if you'd rather only flag buttons that actually live inside a <form> at runtime (where the default-submit matters), disable the rule project-wide and rely on review / a custom lint:
    { "rules": { "no-implicit-button-type": "off" } }

When debugging a false positive:

node node_modules/html-validate-ember/dump-blanked.js path/to/file.gts

Prints the original <template> body and the length-equivalent HTML the transformer hands to html-validate. False positives are usually traceable to the blanker losing or mis-emitting structure.

Contributing

PRs welcome. CI runs pnpm install --frozen-lockfile, pnpm run build, pnpm run typecheck:tests, and pnpm test — mirror those locally.