๐Ÿ› ๏ธ Chapter 93: HTML Tooling, Linting & Quality Assurance

Markuplint: Modern Modular HTML Linter

Abstract Syntax Tree (AST) markup analysis, framework-aware linting for React JSX, Vue SFCs, and Svelte, strict WAI-ARIA validation, and custom element containment rules.

LEARNING OBJECTIVES โŒต
  • Understand how Markuplint uses an Abstract Syntax Tree (AST) and HTML spec models to perform contextual markup analysis.
  • Configure .markuplintrc across 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 (nodeRules and childNodeRules) for targeted design system constraints.
๐ŸŽฌ INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
๐ŸŒ
1. Input
Directives & Tags
โš™๏ธ
2. Parse
Tokenizer & AST
๐ŸŒณ
3. Layout
Box Model & Flow
๐ŸŽจ
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

๐Ÿ“– 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?

  1. Full Component Framework Support: Integrates directly with JSX, TSX, Vue Single File Components (.vue), Svelte (.svelte), Astro, and template engines (Pug, EJS).
  2. WHATWG Permitted Content Engine: Detects illegal element nesting (e.g., <dt> placed outside <dl>, <a> containing <a>, or <button> inside <summary>).
  3. Deep ARIA & A11y Verification: Verifies whether an ARIA role is valid on a given native HTML element according to the ARIA in HTML specification.
  4. 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> violates use-header-level-step. Heading levels communicate document hierarchy to screen readers and must increment sequentially (h1 -> h2).
  • Lines 12โ€“14: Adding role="article" and aria-checked="true" to a <button> violates wai-aria. A button cannot assume an article landmark role, and aria-checked is only valid on role="checkbox", role="radio", or role="switch".
  • Lines 17โ€“21: Placing a <div> directly inside a <ul> violates permitted-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" violates no-refer-to-non-existent-id because no element with id="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:

  1. Refactor the provided Vue Single File Component template to eliminate all Markuplint errors.
  2. Fix heading order hierarchy.
  3. Fix invalid list containment.
  4. Replace invalid ARIA roles/states with accessible semantic attributes.
  5. Provide a valid element for the aria-describedby target.

๐Ÿ Starter Code Sandbox (SettingsModal.vue)

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45ยฐC
INSPECTING DOM: VALID
TAGS: SCANNING...

โš ๏ธ Common Pitfalls

  1. 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.
  2. 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.
  3. 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

  1. Use nodeRules for Design System Component Exceptions: If your design system encapsulates custom elements (e.g., <ds-button>), configure Markuplint's nodeRules or custom spec extensions to declare its valid content model.
  2. Enable In-Editor Linting: Install the official Markuplint VS Code extension (monorail.vscode-markuplint) to receive immediate AST error diagnostics inside .vue, .svelte, and .tsx files.

๐Ÿ“Œ 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-step rule prevents skipped heading ranks, ensuring structured navigation for screen reader users.
  • The wai-aria rule flags invalid, forbidden, or conflicting ARIA roles, states, and properties on native HTML elements.
  • Node-scoped configurations (nodeRules and childNodeRules) allow granular policy customization across different components and sections.
  • --
โญ LEARN: HTML ๐ŸŒŸ โš”๏ธ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why does Markuplint flag <dl><span class="label">Status</span><dd>Active</dd></dl> as an error?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

What error will Markuplint's use-header-level-step rule report on the following sequence: <h1>Page Title</h1> followed immediately by <h3>Section Title</h3>?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

How does Markuplint evaluate template files in Vue (.vue) and React (.tsx)?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP