LEARNING OBJECTIVES โต
- Understand how Markuplint uses an Abstract Syntax Tree (AST) and HTML spec models to perform contextual markup analysis.
- Configure
.markuplintrcacross multi-framework architectures (HTML, React JSX/TSX, Vue SFCs, and Svelte components). - Enforce heading structure integrity (
use-header-level-step) and permitted content models. - Automate WAI-ARIA role and state validation with
@markuplint/rule-wai-aria. - Apply contextual node-scoped rules (
nodeRulesandchildNodeRules) for targeted design system constraints.
๐ The Mental Model & Story (Intuitive Foundation)
Legacy HTML linters analyze code like a simple spellchecker scanning strings of text line by line. If a spellchecker sees the string <p> followed later by </p>, it checks a box. But it does not understand the semantic relationships or content models of the document. It doesn't know that placing a <div> inside a <p> breaks the WHATWG content model, nor does it understand whether aria-expanded="true" is legally permitted on a <span role="heading">.
Markuplint operates like an intelligent compiler and static type checker for markup. It parses the code into a rich Abstract Syntax Tree (AST), understands the full WHATWG specification and W3C WAI-ARIA state machine, and evaluates component templates in React, Vue, and Svelte just as accurately as raw .html files.
[Source Code]
(HTML, JSX, Vue, Svelte)
|
v
+--------------------+
| Framework Parser | (e.g., @markuplint/jsx-parser, @markuplint/vue-parser)
+--------------------+
|
v
+--------------------+
| Markuplint AST | (Understands Elements, Attributes, Roles & Nesting)
+--------------------+
|
+--------+--------+
| |
v v
[WHATWG Spec] [WAI-ARIA 1.2 Spec]
(Content Models) (Permitted Roles/States)
| |
+--------+--------+
|
v
[Diagnostic Output]
Technical Deep Dive & Specifications
Why Markuplint Over Traditional Linters?
- Full Component Framework Support: Integrates directly with JSX, TSX, Vue Single File Components (
.vue), Svelte (.svelte), Astro, and template engines (Pug, EJS). - WHATWG Permitted Content Engine: Detects illegal element nesting (e.g.,
<dt>placed outside<dl>,<a>containing<a>, or<button>inside<summary>). - Deep ARIA & A11y Verification: Verifies whether an ARIA role is valid on a given native HTML element according to the ARIA in HTML specification.
- Scoping via
nodeRules&childNodeRules: Allows customizing rules for specific DOM subtrees (e.g., relaxing rules inside third-party widgets).
Configuration File (.markuplintrc.json)
{
"extends": [
"markuplint:recommended"
],
"parser": {
"\\.jsx?$": "@markuplint/jsx-parser",
"\\.tsx?$": "@markuplint/jsx-parser",
"\\.vue$": "@markuplint/vue-parser",
"\\.svelte$": "@markuplint/svelte-parser"
},
"rules": {
"wai-aria": true,
"permitted-contents": true,
"use-header-level-step": true,
"invalid-attr": true,
"required-attr": true,
"doctype": "always",
"landmark-roles": true,
"no-refer-to-non-existent-id": true
},
"nodeRules": [
{
"selector": ".legacy-widget",
"rules": {
"use-header-level-step": false
}
}
]
}
Key Markuplint Rules Explained
| Rule Name | What It Enforces | Real-World Failure Caught |
|---|---|---|
wai-aria |
Validates ARIA roles, states, and properties against WAI-ARIA 1.2 and ARIA in HTML specs. | Flags <button role="heading"> or <input aria-expanded="true"> on non-expandable inputs. |
permitted-contents |
Enforces WHATWG element containment rules. | Flags <p><div>...</div></p> or <ul><p>...</p></ul>. |
use-header-level-step |
Enforces sequential heading progression (h1 -> h2 -> h3). |
Flags skipping from <h1> directly to <h4>, which disorients screen reader landmark navigation. |
no-refer-to-non-existent-id |
Validates that aria-labelledby, aria-describedby, and <label for> refer to an existing DOM id. |
Flags <label for="missing-input-id"> when the ID was deleted during a refactor. |
invalid-attr |
Flags deprecated or invalid attributes on native HTML elements. | Flags align="center" or border="0" on modern HTML5 elements. |
CLI Execution & Scripts
# Install core and framework parsers
npm install --save-dev markuplint @markuplint/jsx-parser @markuplint/vue-parser
# Execute across all frontend source files
npx markuplint "src/**/*.{html,jsx,tsx,vue,svelte}"
๐ป Interactive Code Playground
Starter Code (React JSX Component: UserProfile.jsx)
Line-by-Line Code Breakdown
- Line 9: Skipping from
<h1>directly to<h4>violatesuse-header-level-step. Heading levels communicate document hierarchy to screen readers and must increment sequentially (h1->h2). - Lines 12โ14: Adding
role="article"andaria-checked="true"to a<button>violateswai-aria. A button cannot assume an article landmark role, andaria-checkedis only valid onrole="checkbox",role="radio", orrole="switch". - Lines 17โ21: Placing a
<div>directly inside a<ul>violatespermitted-contents. The WHATWG specification strictly mandates that<ul>and<ol>may only contain<li>,<script>, or<template>children. - Lines 24โ28:
aria-describedby="tooltip-missing-id"violatesno-refer-to-non-existent-idbecause no element withid="tooltip-missing-id"exists in the component.
Expected Terminal Output from Markuplint CLI
// UserProfile.jsx
import React from 'react';
export function UserProfile({ user }) {
return (
<article className="user-profile">
{/* Violation 1: Sequential heading step skipped (h1 -> h4) */}
<h1>User Profile</h1>
<h4>Account Overview</h4>
{/* Violation 2: Invalid ARIA role on interactive button */}
<button role="article" aria-checked="true">
View Activity
</button>
{/* Violation 3: Permitted content violation (div directly in ul) */}
<ul className="stats-list">
<div className="stat-item">
<span>Reputation: {user.reputation}</span>
</div>
</ul>
{/* Violation 4: aria-describedby references non-existent ID */}
<input
type="text"
aria-describedby="tooltip-missing-id"
placeholder="Update display name"
/>
</article>
);
}$ npx markuplint "src/UserProfile.jsx"
src/UserProfile.jsx:9:7
9:7 error Expected h2, h3, but received h4 use-header-level-step
src/UserProfile.jsx:12:7
12:7 error The "article" role cannot be applied to the <button> element wai-aria
12:7 error The "aria-checked" attribute cannot be used on <button> without an appropriate role wai-aria
src/UserProfile.jsx:17:9
17:9 error The <div> element is not allowed in <ul> permitted-contents
src/UserProfile.jsx:24:7
24:7 error Cannot find the element "#tooltip-missing-id" referenced by "aria-describedby" no-refer-to-non-existent-id
โ 5 errors๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Fix the Framework Component Markuplint Violations
Instructions:
- Refactor the provided Vue Single File Component template to eliminate all Markuplint errors.
- Fix heading order hierarchy.
- Fix invalid list containment.
- Replace invalid ARIA roles/states with accessible semantic attributes.
- Provide a valid element for the
aria-describedbytarget.
๐ Starter Code Sandbox (SettingsModal.vue)
โ ๏ธ Common Pitfalls
- Using ESLint Alone for JSX Markup Audits: Standard ESLint focuses on JavaScript logic and basic JSX syntax, but lacks WHATWG content model parsing (it won't catch
<ul><div></div></ul>). Use Markuplint alongside ESLint. - Skipping Heading Levels for Visual Sizing: Changing an
<h2>to<h5>simply because the default CSS font size looks smaller is an accessibility anti-pattern. Use CSS classes (e.g.class="text-sm") while keeping semantic heading hierarchy intact. - Assigning Redundant ARIA Roles: Writing
<nav role="navigation">or<button role="button">is redundant and flagged by Markuplint because modern browsers provide implicit semantic roles automatically.
๐ก Pro Tips
- Use
nodeRulesfor Design System Component Exceptions: If your design system encapsulates custom elements (e.g.,<ds-button>), configure Markuplint'snodeRulesor custom spec extensions to declare its valid content model. - Enable In-Editor Linting: Install the official Markuplint VS Code extension (
monorail.vscode-markuplint) to receive immediate AST error diagnostics inside.vue,.svelte, and.tsxfiles.
๐ Key Takeaways
- Markuplint is an AST-based markup linter designed specifically for modern web architectures and component frameworks (React JSX, Vue, Svelte, Astro).
- It parses templates against the official WHATWG HTML Content Model and W3C WAI-ARIA 1.2 specifications.
- The
use-header-level-steprule prevents skipped heading ranks, ensuring structured navigation for screen reader users. - The
wai-ariarule flags invalid, forbidden, or conflicting ARIA roles, states, and properties on native HTML elements. - Node-scoped configurations (
nodeRulesandchildNodeRules) allow granular policy customization across different components and sections. - --