Skip to content

Checks ​

This page details how to use checks that are included for accessibility, security, and avoiding typos that would cause unintended HTML.

Setup ​

ts
import { check } from '@itsy/html/check';

app.get('/', (req, res) => {
  const view = Page(data); // a broken template throws HtmlError here

  // the whole page: ids, blocked URLs, accessibility. [] in production
  for (const p of check(view)) console.warn('rule' in p ? p.rule : `E${p.code}`, p.message, p.near);

  res.send(view.markup);
});
What checks are run where
run bychecksresultbuild
html3, 5, 6, 7: an on* attribute, an unquoted attribute value, a plain string inside a tag or <script>, an object or Promise as a valuethrows HtmlErrorboth
html8–14: markup the browser would repairthrows HtmlErrordevelopment only
attrs2, 3: an object for a plain attribute, an on* namethrows HtmlErrorboth
frame, wrap6, 17, 18: a <script> body that is not Html, an invalid tag name, a body on a void elementthrows HtmlErrorboth
check()8–16, 19: the markup check across templates, ids, blocked URLsa Problem for eachdevelopment only, [] in production
check()accessibility rules, on by defaulta Finding for eachdevelopment only, [] in production

HtmlError.code is the same in both builds; production's message is E plus the code. Bundlers pick the development build with the development condition — bundlers and editors.

Markup check ​

Browsers repair broken HTML silently, which can have unintended effects.

Codes 8 to 14 call out these errors - for example, if a tag isn't closed, a closing mark (/>) on an element that does not self-close, etc.

ts
html`<div><p>x</p>`;
txt
HtmlError E9: `<div>` is never closed; a deliberately unmatched tag belongs in raw(), near "<div><p>x</p>"

If a tag needs to be left open so it can be closed elsewhere - use raw.

ts
html`${raw('<main class="page">')}<h1>${'title'}</h1>`;
html
<main class="page"><h1>title</h1>

check() ​

Validates the rules detailed above, plus id references, duplicate ids, and blocked URLs.

ts
check('<label for="email">Email</label><div id="x"></div><div id="x"></div><a href="about:blank#blocked">x</a>');
txt
E15: for="email" refers to an id that is not in the markup — near "<label for="email">Email</labe"
E16: id "x" is used twice — near "="email">Email</label><div id="x"></div><div id="x"></div><a href="abo"
E19: href="about:blank#blocked": the URL guard blocked this value's scheme — near "bel><div id="x"></div><div id="x"></div><a href="about:blank#blocked">"

Pass { ids: false } when the markup is a partial-fragment, e.g. a for or aria-* reference is outside the markup being passed.

Accessibility ​

See more details about what rules are available here.

ts
check('<img src="cat.jpg"><button><svg></svg></button><a href="/skip" aria-hidden="true">Skip</a>');
txt
img-alt: `<img>` has no `alt`: a screen reader reads out the file name instead. Write `alt=""` if the image is decoration — near "<img src="cat.jpg"><button><sv"
empty-button: `<button>` has no text: a screen reader says only "button" — near "<img src="cat.jpg"><button><svg></svg></button><a"
aria-hidden-focus: `<a aria-hidden="true">` can still be tabbed to: focus stops here and a screen reader announces nothing — near "c="cat.jpg"><button><svg></svg></button><a href="/skip" aria-hidden="t"

a11y: { without: [...] } turns a rule off for a whole codebase, and a11y: false turns the lot off. The names are typed, so a typo is a type error.

ts
check('<img src="photo-3.png" alt="photo-3.png">', { a11y: { without: ['img-alt-filename'] } });
json
[]

Custom rules ​

rules runs a project's rules in the same pass and reports them into the same list. The hooks.

ts
check('<p style="color:red">x</p>', {
  a11y: false,
  rules: (report) => ({
    open: (tag, attrs, at) => attrs.has('style') && report('no-inline-style', 'use a utility class', at),
  }),
});
txt
no-inline-style: use a utility class — near "<p style="color:red">x</p>"

MIT licensed.