Skip to main content
Version: Next

@editora/content-a11y

Accessibility checks for the content people write: alt text, headings, link text, lists, tables, colour contrast and more, each mapped to WCAG 2.2, with fixes. It runs in any editor (problems appear next to the content), on a server, and in CI, so inaccessible content is caught before it is published. No dependencies; about 13 kB gzipped.

Install​

npm i @editora/content-a11y

The CLI and server-side checks parse HTML with jsdom (or linkedom), an optional peer dependency: npm i -D jsdom.

Page scanners such as axe check the whole rendered page and the code around it. This checks the part authors control, in the editor, and explains each problem in plain words.

import { checkContent, fixContent } from '@editora/content-a11y';

const { issues, errors } = checkContent(html, { minHeadingLevel: 2 });
// issues: [{ rule, severity, level, wcag, message, help, element, snippet, fixes }]

const { html: fixed, applied } = fixContent(html); // only the fixes that are safe without looking

Rules​

RuleChecksWCAGFix
image-alt-missingImages without alt text1.1.1 Awrite alt text / mark decorative
image-alt-filenameAlt text that is a file name or "image", "photo"...1.1.1 Awrite alt text / mark decorative
image-alt-redundantAlt text starting "Image of..."best practiceauto: drop the prefix
image-alt-longAlt text over 150 charactersbest practicerewrite
image-link-altLinked images with no alt, so the link has no name2.4.4 Asay where the link goes
iframe-titleEmbeds (videos, maps) without a title4.1.2 Aadd a title
media-captionsVideos without a captions track1.2.2 A
link-emptyLinks with no text2.4.4 Aauto: remove the empty link
link-generic-text"click here", "read more", "here"...2.4.4 Arewrite the link text
link-url-textLong web addresses as link textbest practiceuse a description
link-new-windowLinks that open a new tab without saying so3.2.5 AAAadd "(opens in a new tab)" / same tab
link-file-typeLinks to PDF, Word, Excel... without the file typebest practiceadd "(PDF)"
link-same-textThe same link text going to different places2.4.9 AAA
heading-emptyEmpty headings1.3.1 Aauto: remove
heading-skipSkipped heading levels (h2 then h4)1.3.1 Achange the level
heading-too-highHeadings above the content's starting level (with minHeadingLevel)best practicechange the level
heading-fakeShort bold paragraphs used as headings1.3.1 Amake it a heading
heading-longHeadings over 160 charactersbest practicemake it a paragraph
list-fakeLines typed with "-" or "1." instead of a list1.3.1 Amake it a list
table-headersData tables without header cells1.3.1 Amake the first row the header
table-empty-headerEmpty header cells1.3.1 A
text-contrastText colours set in the content with too little contrast1.4.3 AAnearest passing colour
text-all-capsLong runs of capital lettersbest practicesentence case
text-underlineUnderlined text that is not a linkbest practiceremove the underline
empty-paragraphsEmpty paragraphs used for spacingbest practiceauto: remove

Fixes. auto fixes are safe without looking, and are the only ones fixContent() applies unless you pass kinds: ['auto', 'suggested']. Fixes that need words from a person (alt text, link text) take them as input, and fixes that depend on what the content means (marking an image decorative, opening a link in the same tab) are only ever applied by a person; never in bulk.

Contrast is checked for colours set in the content itself (inline styles from an editor's colour picker). Colours from your theme are the theme's job; pass background if your content sits on a non-white background.

In an editor​

import { attachContentChecker } from '@editora/content-a11y';

const checker = attachContentChecker(document.querySelector('#editor'), {
minHeadingLevel: 2,
onChange: ({ errors, warnings }) => updateBadge(errors + warnings),
onFix: ({ beforeHTML }) => history.record(beforeHTML), // for your undo stack
});

Markers appear next to each problem without changing the content's HTML. A marker opens the explanation, the WCAG criterion and the fixes; Escape closes it and arrow keys move between markers. It re-checks as people type.

Editora​

import { ContentA11yPlugin } from '@editora/content-a11y/editora';

editor.setConfig({
plugins: ['heading', 'bold', 'link', 'table', 'history', ContentA11yPlugin({ minHeadingLevel: 2 })],
toolbar: 'undo redo | heading bold link table | toggleContentA11y',
});

The toolbar button shows the number of problems and turns the markers on. Every fix is one undo step. Commands: toggleContentA11y, fixContentA11y (apply the safe fixes), getContentA11yResult.

In CI​

npm i -D @editora/content-a11y jsdom
npx content-a11y content/ --min-heading 2 # exits 1 on errors
npx content-a11y content/ --fix # apply the safe fixes in place
npx content-a11y page.html --json > a11y-report.json
cat body.html | npx content-a11y - --fail-on warning

Options: --fix, --fix-suggested, --json, --min-heading <n>, --fail-on error|warning|none, --rule <id>=off|error|warning|info (repeatable). Folders are searched for .html files. Full documents keep their <head> when fixed.

On a server, checkContent needs an HTML parser for strings: checkContent(html, { parse: (h) => new JSDOM(h).window.document }).

Options​

OptionDefault
minHeadingLevelnoneWhere the content's headings start. Pass 2 when the page title is the h1
rules{ 'text-all-caps': 'off', 'link-new-window': 'error' }
maxAltLength150
background#ffffffThe page background behind the content, for contrast
genericLinkTextMore phrases to treat as meaningless link text
fileTypespdf, doc(x), xls(x), ppt(x), odt, zip
messagesReplace a rule's message (e.g. to translate)
parseDOMParser

Demo​

From a clone of the Editora repository:

npm run build -w @editora/core && npm run build -w @editora/content-a11y
node packages/content-a11y/demo/server.mjs # http://localhost:5196