Skip to content

Search tags, filters, functions and guides.

Tooling / HTML checks

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.

CodeReportsKind
html-obsolete-elementAn element the HTML standard has made obsolete. See Obsolete elementsSpec
html-obsolete-attributeAn attribute the HTML standard has made obsolete, such as align or bgcolor. See Obsolete attributesSpec
html-attribute-valueA value an attribute doesn't accept, such as type="emial", or a boolean attribute set to "false". See Attribute valuesSpec
html-img-altAn <img> with no alt attributeSpec
html-button-typeA <button> with no type attributeAdvice
html-langAn <html> element with no lang attribute, or a lang that isn't a valid language tagAdvice
html-iframe-titleAn <iframe> with no title attributeAdvice
html-viewport-zoomA viewport <meta> that stops people zoomingAdvice
html-tabindexA tabindex above 0Advice
html-aria-attributeAn aria- attribute that WAI-ARIA doesn't define, such as aria-labeledbySpec
html-roleA role that WAI-ARIA doesn't define, or an abstract role such as widget that pages must not useSpec
html-redundant-roleA role an element already has, such as <nav role="navigation">Advice
html-aria-hidden-focusablearia-hidden="true" on an element people can tab toSpec
html-heading-orderA heading that skips a level, such as an <h4> straight after an <h2>Advice
html-nested-interactiveA link, button or other interactive element inside an <a> or a <button>, or a <label> inside a <label>Spec
html-permitted-contentAn element its parent can't hold, such as a <div> in a <p> or an <li> outside a list. See NestingSpec

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.

ElementUse instead
acronymabbr
appletobject or embed
basefont, big, blink, center, font, marquee, multicol, nobr, spacerCSS
bgsoundaudio
dirul
frame, frameset, noframesiframe, or a layout with CSS
isindexA form with a text field
keygenThe Web Crypto API
listing, plaintext, xmppre and code
menuitemA button inside menu
nextidNothing. Remove it
noembedFallback content inside object
paramAttributes on object
rb, rtcruby with rt
strikes, or del for removed text
ttcode, 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>.

ElementObsolete attributes
acharset, coords, methods, name, rev, shape, urn
areanohref, type
bodyalink, background, bgcolor, link, text, vlink, and the margin attributes, such as marginwidth and topmargin
brclear
caption, div, h1 to h6, legend, palign
col, tbody, thead, tfoot, tralign, char, charoff, valign, plus width on col and bgcolor on tr
td, thabbr (on td only), align, axis, bgcolor, char, charoff, height, nowrap, scope (on td only), valign, width
dl, menu, olcompact
ulcompact, type
litype
embedalign, hspace, name, vspace
headprofile
hralign, color, noshade, size, width
htmlmanifest, version
iframealign, allowtransparency, frameborder, framespacing, hspace, longdesc, marginheight, marginwidth, scrolling, vspace
imgalign, border, hspace, longdesc, lowsrc, name, vspace
inputalign, hspace, ismap, usemap, vspace
linkcharset, rev, target
metascheme
objectalign, archive, border, classid, code, codebase, codetype, declare, hspace, standby, vspace
optionname
prewidth
scriptcharset, event, for, language
tablealign, 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.

AttributeOnValues
typeinputbutton, checkbox, color, date, datetime-local, email, file, hidden, image, month, number, password, radio, range, reset, search, submit, tel, text, time, url, week
typebuttonbutton, reset, submit
typeol1, a, A, i, I
method, formmethodform, and button and inputget, post, dialog
enctype, formenctypeform, and button and inputapplication/x-www-form-urlencoded, multipart/form-data, text/plain
autocompleteformon, off
loadingimg, iframeeager, lazy
decodingimgasync, auto, sync
fetchpriorityimg, iframe, link, scriptauto, high, low
crossoriginaudio, img, link, script, videoEmpty, anonymous, use-credentials
referrerpolicya, area, iframe, img, link, scriptEmpty, no-referrer, no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
target, formtargeta, area, base, form, and button and inputAny name, or a keyword: _blank, _parent, _self, _top, _unfencedTop. Names can't start with _, so target="_new" is reported
preloadaudio, videoEmpty, auto, metadata, none
scopethcol, colgroup, row, rowgroup
kindtrackcaptions, chapters, descriptions, metadata, subtitles
wraptextareahard, soft
shapeareacircle, default, poly, rect
captureinputenvironment, user
sandboxiframeAny 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
blockinglink, script, stylerender
popovertargetactionbutton, inputhide, show, toggle
commandbuttonclose, hide-popover, request-close, show-modal, show-popover, toggle-popover, or a custom command starting with --
shadowrootmodetemplateclosed, open
dirAny elementauto, ltr, rtl
hiddenAny elementEmpty, hidden, until-found
contenteditableAny elementEmpty, false, plaintext-only, true
draggableAny elementfalse, true
spellcheckAny elementEmpty, false, true
translateAny elementEmpty, no, yes
autocapitalizeAny elementcharacters, none, off, on, sentences, words
inputmodeAny elementdecimal, email, none, numeric, search, tel, text, url
enterkeyhintAny elementdone, enter, go, next, previous, search, send
popoverAny elementEmpty, 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 if image.alt turns 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 or verbatim.
  • 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, island or component, and of a page's blocks. Rules that need the parent skip those elements.
  • If the branches of an if leave different elements open, or a for loop 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, slot or block may 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 if whose branches end on different headings, or a for loop 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.