Tooling
HTML checks
Warnings for obsolete and inaccessible HTML, before you render.
check warns about HTML that is invalid, obsolete or hard to use with assistive technology, such as an image with no alt, a <div> inside a <p> or a skipped heading level. The language server reports the same warnings as you type. Nothing is checked while rendering, so pages render just as fast.
components/card.kiln:
<center>
<img src="{{ image.src }}">
<button>Read more</button>
</center>
Warnings:
html-obsolete-element 1:1 <center> is obsolete. Use CSS instead
html-img-alt 2:2 <img> has no alt attribute. Use alt="" if the image is decorative
html-button-type 3:2 <button> has no type attribute, so it submits any form it is in. Add type="button" or type="submit"
Each warning is a normal check diagnostic, with a stable code, a message, a severity and a position.
Rules
The rules follow the HTML standard and WAI-ARIA, as the MDN HTML reference describes them. Spec rules report HTML that the standards say is wrong. Advice rules report HTML that is valid but works badly for people, following MDN's guidance and WCAG.
| Code | Reports | Kind |
|---|---|---|
html-obsolete-element | An element the HTML standard has made obsolete. See Obsolete elements | Spec |
html-obsolete-attribute | An attribute the HTML standard has made obsolete, such as align or bgcolor. See Obsolete attributes | Spec |
html-attribute-value | A value an attribute doesn't accept, such as type="emial", or a boolean attribute set to "false". See Attribute values | Spec |
html-img-alt | An <img> with no alt attribute | Spec |
html-button-type | A <button> with no type attribute | Advice |
html-lang | An <html> element with no lang attribute, or a lang that isn't a valid language tag | Advice |
html-iframe-title | An <iframe> with no title attribute | Advice |
html-viewport-zoom | A viewport <meta> that stops people zooming | Advice |
html-tabindex | A tabindex above 0 | Advice |
html-aria-attribute | An aria- attribute that WAI-ARIA doesn't define, such as aria-labeledby | Spec |
html-role | A role that WAI-ARIA doesn't define, or an abstract role such as widget that pages must not use | Spec |
html-redundant-role | A role an element already has, such as <nav role="navigation"> | Advice |
html-aria-hidden-focusable | aria-hidden="true" on an element people can tab to | Spec |
html-heading-order | A heading that skips a level, such as an <h4> straight after an <h2> | Advice |
html-nested-interactive | A link, button or other interactive element inside an <a> or a <button>, or a <label> inside a <label> | Spec |
html-permitted-content | An element its parent can't hold, such as a <div> in a <p> or an <li> outside a list. See Nesting | Spec |
Element and attribute names match in any case, as they do in HTML, so <CENTER> is reported too.
Images
Screen readers read an image's alt text in its place. Without alt, many read out the file name instead. Describe what the image shows, or use an empty alt for an image that is only decoration:
<img src="{{ post.image }}" alt="{{ post.imageAlt }}">
<img src="/images/divider.svg" alt="">
Buttons
A <button> inside a form submits it unless it says otherwise. A button that opens a menu or a dialog then submits the form as well. Give every button a type, so moving it into a form later can't change what it does.
Language
lang tells screen readers which voice and pronunciation to use, and tells browsers which language to translate from. Put it on <html>, such as <html lang="en-GB">.
On any element, a written-out lang must be a valid language tag. lang="en_GB" is reported, since language tags join their parts with -. An empty lang is allowed: it means the language is unknown.
Frames
Screen readers announce an <iframe> by its title, so people can decide whether to go into it. Describe what it holds, such as title="Map of our office".
Zooming
People with low vision zoom in to read. A viewport <meta> whose content has user-scalable=no, or a maximum-scale below 2, stops them. WCAG asks that text can be zoomed to 200%.
Tab order
A tabindex above 0 moves an element ahead of everything else when someone tabs through the page, so the order no longer follows what they see. Use 0 to make an element focusable in its normal place, or -1 to focus it only from script. Only written-out numbers are checked.
ARIA
Browsers ignore an aria- attribute or a role they don't know, so a typo quietly does nothing. The rules follow WAI-ARIA 1.2, plus the attributes and roles the 1.3 draft adds, such as aria-description. Roles from DPUB-ARIA and Graphics-ARIA, which start with doc- and graphics-, are accepted.
A role may list several roles as fallbacks, such as role="switch checkbox", and each one is checked. Roles match in any case. Only written-out values are checked.
html-redundant-role reports a role that repeats the role an element always has, such as <button role="button">, <main role="main"> or <a href="/" role="link">. Lists are left alone: <ul role="list"> brings back list semantics that Safari drops from lists styled with list-style: none.
aria-hidden="true" hides an element from screen readers. On a link, button, form field or an element with tabindex="0", people can still tab to it, and hear nothing. Remove it, or make the element unfocusable too. It is checked on the element itself only, and only when Kiln can see all of its attributes.
Obsolete elements
These elements are obsolete in the HTML standard. Browsers may still show them, but they can't be relied on, and most have a better replacement.
| Element | Use instead |
|---|---|
acronym | abbr |
applet | object or embed |
basefont, big, blink, center, font, marquee, multicol, nobr, spacer | CSS |
bgsound | audio |
dir | ul |
frame, frameset, noframes | iframe, or a layout with CSS |
isindex | A form with a text field |
keygen | The Web Crypto API |
listing, plaintext, xmp | pre and code |
menuitem | A button inside menu |
nextid | Nothing. Remove it |
noembed | Fallback content inside object |
param | Attributes on object |
rb, rtc | ruby with rt |
strike | s, or del for removed text |
tt | code, kbd or samp |
Obsolete attributes
These attributes are obsolete in the HTML standard. Most of them set how things look, which CSS now does. The warning says what to use instead when it isn't CSS, such as id in place of <a name>.
| Element | Obsolete attributes |
|---|---|
a | charset, coords, methods, name, rev, shape, urn |
area | nohref, type |
body | alink, background, bgcolor, link, text, vlink, and the margin attributes, such as marginwidth and topmargin |
br | clear |
caption, div, h1 to h6, legend, p | align |
col, tbody, thead, tfoot, tr | align, char, charoff, valign, plus width on col and bgcolor on tr |
td, th | abbr (on td only), align, axis, bgcolor, char, charoff, height, nowrap, scope (on td only), valign, width |
dl, menu, ol | compact |
ul | compact, type |
li | type |
embed | align, hspace, name, vspace |
head | profile |
hr | align, color, noshade, size, width |
html | manifest, version |
iframe | align, allowtransparency, frameborder, framespacing, hspace, longdesc, marginheight, marginwidth, scrolling, vspace |
img | align, border, hspace, longdesc, lowsrc, name, vspace |
input | align, hspace, ismap, usemap, vspace |
link | charset, rev, target |
meta | scheme |
object | align, archive, border, classid, code, codebase, codetype, declare, hspace, standby, vspace |
option | name |
pre | width |
script | charset, event, for, language |
table | align, bgcolor, border, cellpadding, cellspacing, frame, rules, summary, width |
Attribute values
Many attributes only accept certain keywords. A browser treats any other value as the default, so a typo such as type="emial" quietly gives a plain text field. Kiln reports a written-out value that isn't one of these, and suggests the closest. Keywords match in any case, apart from <ol type>, where a and A differ.
| Attribute | On | Values |
|---|---|---|
type | input | button, checkbox, color, date, datetime-local, email, file, hidden, image, month, number, password, radio, range, reset, search, submit, tel, text, time, url, week |
type | button | button, reset, submit |
type | ol | 1, a, A, i, I |
method, formmethod | form, and button and input | get, post, dialog |
enctype, formenctype | form, and button and input | application/x-www-form-urlencoded, multipart/form-data, text/plain |
autocomplete | form | on, off |
loading | img, iframe | eager, lazy |
decoding | img | async, auto, sync |
fetchpriority | img, iframe, link, script | auto, high, low |
crossorigin | audio, img, link, script, video | Empty, anonymous, use-credentials |
referrerpolicy | a, area, iframe, img, link, script | Empty, no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url |
target, formtarget | a, area, base, form, and button and input | Any name, or a keyword: _blank, _parent, _self, _top, _unfencedTop. Names can't start with _, so target="_new" is reported |
preload | audio, video | Empty, auto, metadata, none |
scope | th | col, colgroup, row, rowgroup |
kind | track | captions, chapters, descriptions, metadata, subtitles |
wrap | textarea | hard, soft |
shape | area | circle, default, poly, rect |
capture | input | environment, user |
sandbox | iframe | Any of allow-downloads, allow-forms, allow-modals, allow-orientation-lock, allow-pointer-lock, allow-popups, allow-popups-to-escape-sandbox, allow-presentation, allow-same-origin, allow-scripts, allow-storage-access-by-user-activation, allow-top-navigation, allow-top-navigation-by-user-activation, allow-top-navigation-to-custom-protocols, separated by spaces |
blocking | link, script, style | render |
popovertargetaction | button, input | hide, show, toggle |
command | button | close, hide-popover, request-close, show-modal, show-popover, toggle-popover, or a custom command starting with -- |
shadowrootmode | template | closed, open |
dir | Any element | auto, ltr, rtl |
hidden | Any element | Empty, hidden, until-found |
contenteditable | Any element | Empty, false, plaintext-only, true |
draggable | Any element | false, true |
spellcheck | Any element | Empty, false, true |
translate | Any element | Empty, no, yes |
autocapitalize | Any element | characters, none, off, on, sentences, words |
inputmode | Any element | decimal, email, none, numeric, search, tel, text, url |
enterkeyhint | Any element | done, enter, go, next, previous, search, send |
popover | Any element | Empty, auto, hint, manual |
Custom elements, whose names contain a -, define their own attributes, so only the attributes for any element are checked on them.
Boolean attributes
A boolean attribute such as disabled is on whenever it is there, whatever its value. disabled="false" still disables the button, and autoplay="false" still plays the video. Kiln reports a boolean attribute written with "false", "0", "no" or "off". Leave the attribute out to turn it off, or use attrs, which leaves out false:
<button{{ attrs({ type: "submit", disabled: saving }) }}>Save</button>
The boolean attributes are allowfullscreen, async, autofocus, autoplay, checked, controls, default, defer, disabled, formnovalidate, inert, ismap, itemscope, loop, multiple, muted, nomodule, novalidate, open, playsinline, readonly, required, reversed, selected, shadowrootclonable, shadowrootdelegatesfocus and shadowrootserializable, on the elements that have them.
Nesting
Some rules look at which elements are open around each one. Kiln follows the browser's rules for end tags HTML lets you leave out, such as </li>, </p> and </td>.
Heading order
Screen reader users move around a page by its headings, and the levels tell them how sections fit together. A heading may go back up any number of levels, but should only go down one at a time, so an <h2> is followed by an <h3>, not an <h4>.
The first heading in a template can be any level, since a component may start at <h3>. After that, each heading is compared with the one before it in the same template.
Interactive elements
A link or button can't contain another link, button, form field, <details>, <iframe> or an element with tabindex. Browsers handle it differently, and keyboard and screen reader users often can't reach the inner one. A <label> can't contain another <label>.
Permitted content
html-permitted-content follows the "Permitted content" and "Permitted parents" of each element on MDN. It reports:
- An element that only holds text and inline elements, such as
<p>,<span>,<button>or a heading, holding a block, such as a<div>,<ul>or<h2>. A<p>doesn't hold it at all: the browser ends the paragraph before it. - Children a parent can't hold: only
<li>in<ul>,<ol>and<menu>;<dt>,<dd>and<div>in<dl>; table parts in<table>, rows in<thead>,<tbody>and<tfoot>, and cells in<tr>;<source>and<img>in<picture>; headings and<p>in<hgroup>; and<col>in<colgroup>.<script>and<template>are allowed in all of these. - An element outside the parent it needs:
<li>outside a list,<dt>and<dd>outside<dl>, table parts outside a table,<summary>outside<details>,<legend>outside<fieldset>,<figcaption>outside<figure>,<source>outside<picture>or a media element,<track>outside a media element, and<rt>and<rp>outside<ruby>. - A
<summary>or<legend>that isn't the first child of its<details>or<fieldset>. - A
<form>inside a<form>, which the browser ignores, and a<header>or<footer>inside a<header>or<footer>. - A
<main>inside anything other than<html>,<body>,<div>,<form>or a custom element.
What <select>, <optgroup> and <option> may hold has grown with customisable selects, so they aren't checked.
What Kiln can see
The rules read the HTML written in the template, not the page it renders. Kiln only warns when it is sure, so it may miss a problem but shouldn't report one that isn't there.
- An attribute whose value comes from
{{ }}counts as present.alt="{{ image.alt }}"passes even ifimage.altturns out empty. - When a tag or an output such as
attrs(...)sits among an element's attributes, Kiln can't tell which attributes the element ends up with. It doesn't report a missing attribute on that element, but still checks the attributes it can see. A comment there makes no difference. - An attribute whose name is partly built at render time, such as
aria-{{ name }}, isn't checked. - An element whose name is built at render time, such as
<{{ tag }}>, isn't checked. - Content inside
<svg>and<math>isn't checked. SVG and MathML have elements of their own, such as SVG's<font>. - Content inside elements that hold text rather than HTML, such as
<script>,<style>,<textarea>and<title>, isn't checked. Nor are HTML comments, Kiln comments orverbatim. - Each template is checked on its own. A template it includes is checked when you check that template.
- A template that starts with an XML declaration, such as a feed or a sitemap, isn't checked.
For nesting and heading order:
- Only elements written in the same template count. A template's top-level content could end up anywhere, so its parent is unknown, and so is the parent of the content of a
macro,fill,set,apply,add,islandorcomponent, and of a page's blocks. Rules that need the parent skip those elements. - If the branches of an
ifleave different elements open, or aforloop opens an element it doesn't close, Kiln no longer knows which elements are open at that point. Elements opened after it are checked again. - An
include,component,island,slotorblockmay hold headings, so the heading after it is compared with nothing. So is the heading after an output that may hold headings, such as{{ post.body|raw }}. An output inside an element that can't hold headings, such as a<p>, doesn't count. - The same goes for the heading after an
ifwhose branches end on different headings, or aforloop with headings in it.
{# Not reported: the if may add alt #}
<img src="{{ src }}" {% if alt %}alt="{{ alt }}"{% endif %}>
{# Not reported: attrs may add type #}
<button{{ attrs(attributes) }}>{{ label }}</button>
{# Reported: aria-labeledby is a typo for aria-labelledby #}
<nav aria-labeledby="site-nav-title"{{ attrs(attributes) }}>
Some problems need the whole rendered page, such as heading order across a layout, a page and its components, repeated id values, a form field without a label, or more than one <main>. Kiln doesn't check these. Run an HTML validator or an accessibility checker over your rendered pages for them.
Turning rules off
The rules are on by default, and each reports a warning. Use the htmlChecks option to change that. Give a rule "off", "warning" or "error". Rules you leave out stay as warnings.
const kiln = createKiln({
loader,
htmlChecks: {
"html-button-type": "off",
"html-img-alt": "error",
},
});
Set htmlChecks: false to turn every rule off, such as for an environment that only renders plain-text emails. These rules don't depend on strict mode.
To give your editor the same settings, add htmlChecks to kiln.json. generateManifest copies it from your environment.