HTML and Semantics skill 1 of 4
Semantic HTML SKILL.md: Audit and Fix Markup Without Breaking the Page
The Semantic HTML skill makes Claude, Cursor or another AI agent audit existing markup and fix it safely. A read-only checker flags clickable divs, links used as buttons, unnamed controls, skipped headings and missing landmarks. The agent then applies fixed rules and changes code only in small batches you approve.
semantic-html/SKILL.md, version 1.0.0, 4,316 words, plus scripts/semantic_check.py.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 23 October 2026. Updated 23 October 2026. Free to use under CC BY 4.0.
Most markup problems are invisible to the person who wrote them. A div with a click handler works with a mouse. A link with href="#" opens the chat window. A heading chosen for its font size looks right. Each one fails for keyboard and screen reader users, and each one is easy to miss in review.
This skill makes an agent audit existing HTML, choose the right element for each problem with fixed decision rules, and change the markup in small, approved batches that keep the page's styling, scripts and tracking working. It runs a short read-only checker first, so the work starts from a list of line numbers instead of a guess.
It works on plain HTML and on the rendered output of any framework. Framework notes and tool facts sit in the appendix.
When to use this skill, and when not to
Use it when the user wants to:
- Audit a page, template or component for semantic problems: missing landmarks, skipped headings, clickable
divs, links used as buttons, unnamed buttons, missingalt. - Refactor "div soup" into meaningful elements.
- Review a pull request or generated UI code for markup quality.
- Decide between
<a>and<button>,sectionanddiv, or a list and plain paragraphs.
Do not use it for these jobs:
| Request | Use instead |
|---|---|
| Labels, autocomplete, validation and errors in forms | html-forms.md |
| Colour contrast, focus styling, motion, or a full WCAG audit | An accessibility audit skill. This skill covers markup only |
| Page titles, meta descriptions and structured data for search | title-tags-meta-descriptions.md, structured-data.md |
| Visual layout and CSS architecture | Not covered |
Guardrails that apply to every run
- Propose, then change. The agent reports and proposes first. It edits files only after the user approves a batch, and only on a branch or working copy under version control. It never commits to the main branch, pushes, merges or deploys.
- Batches stay small. At most 10 files or 30 element changes per batch (defaults). Each batch is shown as a diff before it is applied.
- Behaviour is preserved. Every element swap is checked against the CSS selectors, scripts, tests and analytics triggers that target it (step 5). If a swap would break one and the fix is outside the files in scope, the agent stops and asks.
- Native before ARIA. The agent does not add a
roleto make a non-interactive element behave like a control when a native element exists. - Personal data and secrets. Rendered HTML from logged-in pages can hold names, emails, account numbers, CSRF tokens in hidden inputs and API keys in inline scripts. Run the checker locally. Before sharing any HTML with an external service or pasting it into a prompt, remove inline scripts and the values of hidden inputs, and ask the user. Reports quote line numbers and element names, not page text.
- Nothing is invented. Thresholds in this file are labelled defaults. Tool facts carry a source and the date read. Version-dependent facts carry verify before relying on this.
- Not legal advice. The skill improves markup. It does not certify conformance with WCAG or any accessibility law. If the user needs a compliance statement, route it to a qualified human auditor.
- Scope boundary. The agent refuses to rewrite copy, redesign layouts, remove content, or add ARIA to hide a problem it was asked to fix.
Inputs the agent must have before starting
Ask for what is missing in one message and wait. Do not proceed on assumptions.
| # | Input | What to ask if missing |
|---|---|---|
| 1 | Target | "Which pages, templates or components should I check? Paths or URLs, please." |
| 2 | Stack | "Is this plain HTML, a server template (for example Django, Rails, Twig) or a framework (React, Vue, Svelte, Angular)?" |
| 3 | Rendered output | "Can you save the rendered HTML of each page (browser Save As, or the element's outer HTML from devtools)? The checker reads rendered HTML most reliably." |
| 4 | Safety net | "Is the code in version control, and can I work on a branch?" |
| 5 | Dependants | "Do any CSS files, scripts, tests or tag manager triggers target these elements by tag or class?" |
| 6 | Logged-in pages | "Do any pages show personal or account data when rendered?" |
| 7 | Test tools | "Can you run Python 3 locally, and do you have axe or another automated checker available?" |
If input 4 is no, the agent stops after the report and proposes changes as text only.
Raw SKILL.md file
This is the exact file agents read, including the credit header and licence notice. Save it as semantic-html/SKILL.md.
---
name: semantic-html
description: "Audits existing HTML for semantic problems and fixes them in small approved batches without breaking styling, scripts, tests or analytics. Runs a read-only standard-library checker that flags clickable divs, links used as buttons, unnamed buttons and links, nested controls, skipped headings, missing main landmark, missing lang, unlabelled navs and images without alt, then applies fixed decision rules for links versus buttons, landmarks, headings, alt text, lists, tables and ARIA, and verifies with the checker, a keyboard pass and a screen reader spot check. Works on plain HTML and rendered React, Vue, Svelte or Angular output. Use whenever someone asks to audit or refactor markup, fix div soup, review a pull request for semantic HTML, choose between a link and a button, fix heading order or landmarks, or add alt text. Do NOT use for form labels, autocomplete or validation (use html-forms), full WCAG or colour-contrast audits, legal conformance statements, SEO metadata or CSS layout."
license: CC-BY-4.0
metadata:
author: Afeez Adebayo
author_url: https://afeezadebayo.com
author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
author_x: https://x.com/za_icon
source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/semantic-html/
category: frontend-development
subcategory: html-semantics
version: "1.0.0"
created: "2026-10-23"
updated: "2026-10-23"
---
# Semantic HTML: Audit and Fix Markup Without Breaking the Page
Most markup problems are invisible to the person who wrote them. A `div` with a click handler works with a mouse. A link with `href="#"` opens the chat window. A heading chosen for its font size looks right. Each one fails for keyboard and screen reader users, and each one is easy to miss in review.
This skill makes an agent audit existing HTML, choose the right element for each problem with fixed decision rules, and change the markup in small, approved batches that keep the page's styling, scripts and tracking working. It runs a short read-only checker first, so the work starts from a list of line numbers instead of a guess.
It works on plain HTML and on the rendered output of any framework. Framework notes and tool facts sit in the appendix.
## When to use this skill, and when not to
Use it when the user wants to:
- Audit a page, template or component for semantic problems: missing landmarks, skipped headings, clickable `div`s, links used as buttons, unnamed buttons, missing `alt`.
- Refactor "div soup" into meaningful elements.
- Review a pull request or generated UI code for markup quality.
- Decide between `<a>` and `<button>`, `section` and `div`, or a list and plain paragraphs.
Do not use it for these jobs:
| Request | Use instead |
|---|---|
| Labels, autocomplete, validation and errors in forms | `html-forms.md` |
| Colour contrast, focus styling, motion, or a full WCAG audit | An accessibility audit skill. This skill covers markup only |
| Page titles, meta descriptions and structured data for search | `title-tags-meta-descriptions.md`, `structured-data.md` |
| Visual layout and CSS architecture | Not covered |
## Guardrails that apply to every run
1. **Propose, then change.** The agent reports and proposes first. It edits files only after the user approves a batch, and only on a branch or working copy under version control. It never commits to the main branch, pushes, merges or deploys.
2. **Batches stay small.** At most 10 files or 30 element changes per batch (defaults). Each batch is shown as a diff before it is applied.
3. **Behaviour is preserved.** Every element swap is checked against the CSS selectors, scripts, tests and analytics triggers that target it (step 5). If a swap would break one and the fix is outside the files in scope, the agent stops and asks.
4. **Native before ARIA.** The agent does not add a `role` to make a non-interactive element behave like a control when a native element exists.
5. **Personal data and secrets.** Rendered HTML from logged-in pages can hold names, emails, account numbers, CSRF tokens in hidden inputs and API keys in inline scripts. Run the checker locally. Before sharing any HTML with an external service or pasting it into a prompt, remove inline scripts and the values of hidden inputs, and ask the user. Reports quote line numbers and element names, not page text.
6. **Nothing is invented.** Thresholds in this file are labelled defaults. Tool facts carry a source and the date read. Version-dependent facts carry **verify before relying on this**.
7. **Not legal advice.** The skill improves markup. It does not certify conformance with WCAG or any accessibility law. If the user needs a compliance statement, route it to a qualified human auditor.
8. **Scope boundary.** The agent refuses to rewrite copy, redesign layouts, remove content, or add ARIA to hide a problem it was asked to fix.
## Inputs the agent must have before starting
Ask for what is missing in one message and wait. Do not proceed on assumptions.
| # | Input | What to ask if missing |
|---|---|---|
| 1 | Target | "Which pages, templates or components should I check? Paths or URLs, please." |
| 2 | Stack | "Is this plain HTML, a server template (for example Django, Rails, Twig) or a framework (React, Vue, Svelte, Angular)?" |
| 3 | Rendered output | "Can you save the rendered HTML of each page (browser Save As, or the element's outer HTML from devtools)? The checker reads rendered HTML most reliably." |
| 4 | Safety net | "Is the code in version control, and can I work on a branch?" |
| 5 | Dependants | "Do any CSS files, scripts, tests or tag manager triggers target these elements by tag or class?" |
| 6 | Logged-in pages | "Do any pages show personal or account data when rendered?" |
| 7 | Test tools | "Can you run Python 3 locally, and do you have axe or another automated checker available?" |
If input 4 is no, the agent stops after the report and proposes changes as text only.
## Workflow
### Step 0: Confirm scope and safety
1. Confirm inputs 1 to 7.
2. Confirm the branch. Record the commit the work starts from.
3. Tell the user what will happen: check, triage, decisions, a change plan, approved batches, verification, report.
4. Gate 1: wait for a yes.
### Step 1: Run the checker
1. Save `scripts/semantic_check.py` from this skill folder. It uses only the Python standard library, reads files and writes nothing.
2. Run it on the rendered HTML: `python3 scripts/semantic_check.py page.html`.
3. If only templates are available, run it on them and add "templates only, rendered output not checked" to the report. Template syntax can hide or invent elements.
4. Keep the output. It is the baseline for step 7.
The checker reports these codes. Each line gives the file, line number, level and code.
| Code | Level | Meaning |
|---|---|---|
| `click-on-non-interactive` | error | A `div`, `span`, `li`, `td`, `p`, `img`, `section` or `article` has `onclick`, `@click`, `v-on:click`, `(click)` or `on:click` |
| `link-used-as-button` | error | `href` is empty, `#` or `javascript:` |
| `link-without-href` | error | `<a>` with no `href` cannot take keyboard focus |
| `button-without-name`, `a-without-name` | error | No text, image `alt`, `aria-label`, `aria-labelledby` or `title` |
| `nested-interactive` | error | A link or button inside another link or button |
| `img-missing-alt` | error | `<img>` with no `alt` attribute at all |
| `heading-skip` | error | A heading jumps down more than one level |
| `main-count` | error | Not exactly one `main` landmark |
| `missing-lang` | error | `<html>` has no `lang` |
| `no-h1` | warn | No `h1` on the page |
| `unlabelled-nav` | warn | More than one `nav` and at least one has no label |
| `role-on-non-interactive` | warn | `role="button"` or `role="link"` on a non-interactive element |
| `positive-tabindex` | warn | `tabindex` above 0 changes the tab order |
The checker cannot judge meaning. It does not know whether a heading describes its section or whether `alt` text is useful. Steps 3 and 7 cover that.
### Step 2: Triage
Sort every flag into one of four priority groups. Fix in this order.
| Priority | Codes | Why first |
|---|---|---|
| P1: controls | `click-on-non-interactive`, `link-used-as-button`, `link-without-href`, `*-without-name`, `nested-interactive` | Keyboard and screen reader users cannot reach or understand the control |
| P2: page structure | `main-count`, `missing-lang`, `heading-skip`, `no-h1`, `unlabelled-nav` | Users cannot move around the page or hear it in the right language |
| P3: images | `img-missing-alt` | Content or function is missing for screen reader users |
| P4: order | `positive-tabindex`, `role-on-non-interactive` | Behaviour is unexpected but usually still usable |
Form fields go to `html-forms.md`. List them in the report and do not fix them here.
### Step 3: Decide the element
Use these rules. They are deterministic: the same situation always gets the same element.
**Controls.**
| Situation | Element |
|---|---|
| It changes the URL or loads another page or file | `<a href="real-url">` |
| It does something on this page (opens a dialog, adds to basket, toggles a panel) | `<button type="button">` |
| It submits a form | `<button type="submit">` |
| It shows and hides a section of content with no script needed | `<details>` and `<summary>` |
| It must look like a link but performs an action | `<button type="button">` styled as a link |
| It must look like a button but goes to another page | `<a href>` styled as a button |
The test is "does the URL change?" A link changes the URL. A button does not. Never put a button inside a link, or a link inside a button: pick one element from the table.
**Landmarks.**
| Region | Element | Rule |
|---|---|---|
| Main content | `<main>` | Exactly one per page |
| Site header | `<header>` | Top level of `body`, not inside `main` or `article` |
| Site footer | `<footer>` | Top level of `body` |
| Navigation blocks | `<nav>` | If there is more than one, give each a distinct `aria-label` such as "Main" and "Legal" |
| Related content beside the main content | `<aside>` | Only for content that makes sense apart from the main content |
| A self-contained item (post, product card that stands alone, comment) | `<article>` | It would make sense if lifted on its own |
| A themed group with its own heading | `<section>` | Only when it has a heading. Otherwise use `<div>` |
**Headings.**
1. One `h1` per page, naming what the page is about (default rule).
2. Never skip a level going down: `h2` follows `h1`, `h3` follows `h2`. Going back up any number of levels is fine.
3. Choose the level by the section's place in the outline, never by font size. Change the size in CSS.
4. A heading must describe the content beneath it. A styled sentence that introduces no section is a paragraph.
Do not rely on sectioning elements to change heading levels. The WHATWG spec replaced its outline algorithm in 2022 because no browser implemented it, so an `h1` inside a `section` is still read as an `h1`.
**Images.**
| Image role | `alt` |
|---|---|
| Carries information | Describe the information, not the picture: "Sales doubled between 2024 and 2025" |
| Decorative | `alt=""`, so screen readers skip it |
| The only content of a link or button | Describe the action or destination: "Close", "Home" |
| A complex chart or diagram | A short `alt`, with the full description or data in nearby text |
Never remove the `alt` attribute to mark an image as decorative. A missing `alt` and an empty `alt` behave differently.
**Lists and tables.**
- Items of the same kind in a row or column (menu links, features, steps) go in `ul` or `ol`. A card grid does not need to be a list unless the count or order helps the user.
- A table holds data with rows and columns. Use `th` with `scope="col"` or `scope="row"`. If a table is only for layout, replace it with CSS.
**ARIA.**
1. If a native element does the job, use it and add no role.
2. Add ARIA only for patterns with no native element (tabs, a combobox), and then the element must also get the keyboard behaviour the role promises.
3. `aria-label` on a button or link is for an icon-only control. Do not use it to override visible text.
### Step 4: Write the change plan
For every flag, write one row: file and line, current element, new element, reason (a rule from step 3), and dependants found in step 5. Group the rows into batches of at most 10 files or 30 changes.
### Step 5: Check dependants before each swap
Search the codebase for every tag and class involved. Use the commands in the Templates section.
| Dependant | What breaks | What to do |
|---|---|---|
| CSS selectors by tag (`div.card`, `nav > div`) | Styling disappears | Update the selector in the same batch |
| Button default styles | The new button shows a grey box | Add the button reset from the Templates section |
| Scripts that query by tag or check `tagName` | Handlers stop firing | Update the selector. Keep the class names |
| Event handlers on the old element | Double handling, or Enter fires twice | Remove the manual keyboard handler when moving to a native button |
| `<button>` inside a `<form>` | It submits the form | Always set `type="button"` unless it submits |
| Tests that select by tag or text | Tests fail | Update selectors. Prefer selecting by role and name |
| Tag manager triggers (click classes, element selectors) | Analytics events stop | List them for the user. The agent does not change tag manager settings |
If a dependant sits outside the files in scope, stop and ask the user before changing it.
### Step 6: Apply one batch at a time
1. Show the diff for the batch.
2. Gate 2: wait for a yes for this batch.
3. Apply the batch on the branch.
4. Run step 7 before the next batch.
### Step 7: Verify
1. **Checker.** Re-run `semantic_check.py`. Every flag fixed in the batch must be gone, and no new error may appear.
2. **Keyboard.** Using only the keyboard, Tab through the changed area. Every control receives focus in reading order with a visible focus indicator. Enter activates links. Enter and Space activate buttons.
3. **Screen reader spot check.** Open the headings list and the landmarks list in a screen reader (the rotor in VoiceOver, the elements list in NVDA). Headings form a sensible outline. There is one main region and each navigation has a distinct name.
4. **Automated rules.** Run axe or the team's equivalent on the changed pages. Zero new violations.
5. **Visual.** Compare screenshots before and after. Layout must not shift unless the user approved it.
6. **Tests.** The project's existing tests pass.
Automated checks find only a share of problems. The WebAIM Million 2026 report counted detectable failures on 95.9% of one million home pages and put full conformance at below 4.1%, because only automatically detectable failures were counted. Steps 7.2 and 7.3 are not optional.
### Step 8: Report
Use the report template. State what changed, what is left, what was handed to `html-forms.md`, and what was not checked.
## Worked example
The page below is invented. The checker output is real: it was produced by running the script in this skill on these files.
**Input.** A pricing page, plain HTML, under version control. No CSS targets `div.card` by tag. A tag manager trigger listens for clicks on `.card`.
```html
<!doctype html>
<html>
<head><title>Plans</title></head>
<body>
<div class="top">
<nav><a href="/">Home</a> <a href="/pricing">Pricing</a></nav>
</div>
<div class="content">
<h1>Choose a plan</h1>
<h3>Monthly</h3>
<div class="card" onclick="selectPlan('pro')">Pro plan</div>
<a href="#" onclick="openChat()">Talk to sales</a>
<button class="icon-close"><svg aria-hidden="true"></svg></button>
<img src="/badge.png">
<a href="/terms"><button>Read terms</button></a>
</div>
<div class="footer"><nav><a href="/privacy">Privacy</a></nav></div>
</body>
</html>
```
**Step 1, checker output** (tabs shown as spaces):
```
before.html:1 error main-count 0 main landmarks; expected 1
before.html:1 error missing-lang <html> has no lang
before.html:1 warn unlabelled-nav 2 nav elements, not all labelled
before.html:10 error heading-skip h1 followed by h3
before.html:11 error click-on-non-interactive <div> has a click handler; use <button> or <a href>
before.html:12 error link-used-as-button href="#"; actions belong in <button>
before.html:13 error button-without-name no text, alt or aria-label
before.html:14 error img-missing-alt /badge.png
before.html:15 error nested-interactive <button> inside another link or button
before.html: 8 errors, 1 warnings
```
**Steps 2 and 3, decisions.**
| Line | Decision | Rule |
|---|---|---|
| 11 | `<button type="button" class="card">` | Selecting a plan stays on the page |
| 12 | `<button type="button">` styled as a link | Opening chat does not change the URL |
| 13 | `aria-label="Close"` | Icon-only control |
| 15 | Keep the link, drop the inner button | "Read terms" goes to another page |
| 10 | `h3` becomes `h2` | No skipped levels |
| 14 | `alt=""` | The user confirms the badge is decorative |
| Structure | `header`, `main`, `footer`; `lang="en-GB"`; navs labelled "Main" and "Legal" | Landmark rules |
**Step 5, dependants.** The `.card` class stays, so the tag manager trigger keeps firing. The agent lists it anyway for the user to confirm. The new buttons get the reset styles. `selectPlan` and `openChat` move from inline attributes to listeners on `data-plan` and `data-action`, so the agent shows that script change in the same batch.
**After (one batch, approved):**
```html
<!doctype html>
<html lang="en-GB">
<head><title>Plans</title></head>
<body>
<header>
<nav aria-label="Main"><a href="/">Home</a> <a href="/pricing">Pricing</a></nav>
</header>
<main>
<h1>Choose a plan</h1>
<h2>Monthly</h2>
<button type="button" class="card" data-plan="pro">Pro plan</button>
<button type="button" class="link-style" data-action="chat">Talk to sales</button>
<button type="button" class="icon-close" aria-label="Close"><svg aria-hidden="true"></svg></button>
<img src="/badge.png" alt="">
<a href="/terms">Read terms</a>
</main>
<footer><nav aria-label="Legal"><a href="/privacy">Privacy</a></nav></footer>
</body>
</html>
```
**Step 7, checker output:** `after.html: 0 errors, 0 warnings`. The user then runs the keyboard and screen reader checks and confirms them. The report lists one item for the user: confirm the `.card` click trigger still fires in the tag manager preview.
## Templates
### Checker
`scripts/semantic_check.py` ships with this skill. Usage:
```
python3 scripts/semantic_check.py page.html other.html
```
It exits with status 1 if any error is found, so it can run in CI. Read it before running it.
### Finding dependants
```
grep -rnE "div\.card|\.card\b" --include=*.css --include=*.scss src/
grep -rnE "querySelector(All)?\(['\"][^'\"]*card|tagName" --include=*.js --include=*.ts src/
grep -rnE "getByText|querySelector|\.card" --include=*.test.* --include=*.spec.* .
```
Replace `card` with the class or tag being changed.
### Button reset
```css
button.link-style,
button.card {
appearance: none;
background: none;
border: 0;
padding: 0;
font: inherit;
color: inherit;
text-align: inherit;
cursor: pointer;
}
button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
```
### Keyboard check
```
[ ] Tab reaches every control in the changed area, in reading order
[ ] Focus is visible on every control
[ ] Enter follows every link
[ ] Enter and Space activate every button
[ ] No control is reached that should not be (hidden, decorative)
[ ] Shift+Tab moves back in the same order
```
### Report
```
SEMANTIC HTML REPORT: [files], [date], branch [name] from [commit]
Baseline: [n] errors, [n] warnings
Fixed this run: [n] (P1 [n], P2 [n], P3 [n], P4 [n])
Remaining: [list with reason]
Handed to html-forms: [n] form fields
Dependants changed: [CSS, scripts, tests]
Dependants for the user: [tag manager triggers, files out of scope]
Verification: checker [0 errors], keyboard [pass/fail], screen reader [pass/fail],
automated rules [n new violations], visual [no shift], tests [pass]
Not checked: [rendered output, colour contrast, focus styles beyond changed area]
This report is not a statement of WCAG or legal conformance.
```
### Starting prompt
```
Use the semantic-html skill on [files]. The stack is [stack]. Work on the
branch [name]. Start with the checker and a change plan, and do not edit
anything until I approve a batch.
```
## Failure modes and anti-patterns
| # | Failure | How to spot it | What to do instead |
|---|---|---|---|
| 1 | ARIA patched onto a `div` | `role="button"` and `tabindex="0"` added, but no Enter or Space handling and no disabled state | Replace with `<button type="button">` and a style reset. A native button brings focus, keyboard activation and form behaviour |
| 2 | Heading levels picked for size | `h4` used because it "looks right"; the checker flags `heading-skip` | Pick the level from the outline. Move size into CSS classes |
| 3 | A swap breaks styling, scripts or tracking | Visual diff shows a grey box; a click handler stops; an analytics event stops | Run step 5 before each swap. Keep class names. List tag manager triggers for the user |
| 4 | An automated pass treated as done | The report says "0 errors" and nobody used a keyboard | Run steps 7.2 and 7.3 every time. The checker and axe catch only what can be detected by rule |
| 5 | Links and buttons mixed up | A button inside a link, `href="#"` for actions, a `<button>` that navigates with script | Ask "does the URL change?" and use the table in step 3 |
## Quality bar
Check every item before handing over.
1. The user approved gate 1, and the work is on a named branch.
2. The baseline checker output is saved and quoted in the report.
3. Every flag is in a priority group, fixed, handed to `html-forms.md`, or listed as remaining with a reason.
4. Every element decision cites a rule from step 3.
5. Every batch had at most 10 files or 30 changes and was approved before it was applied.
6. Every `<button>` added has an explicit `type`.
7. Dependants were searched for each changed tag and class, and the result is in the report.
8. The checker shows zero errors in the changed files, with no new errors elsewhere.
9. The keyboard check was run on the changed area and its result is recorded.
10. No `role` was added where a native element exists.
11. No page text, hidden input value or inline script content appears in the report.
12. The report says it is not a conformance statement.
## Cost and effort
The checker runs locally and makes no network calls. The agent reads its output and only the lines it flags, so token use grows with the number of problems, not the size of the page. The cheaper path for a large site is to check templates or shared components first, because one fix there repairs every page that uses them.
## Handoffs to related skills
`html-forms.md` handles every form field the checker or triage surfaces. `core-web-vitals.md` and `javascript-seo.md` cover performance and rendering for search. `structured-data.md` covers schema markup. `ai-evals-testing.md` helps if the team wants to test an agent's markup output at scale.
## Appendix: tool-specific notes
Every fact below was read on 23 October 2026. **Verify before relying on this**: tools and rules change.
### Frameworks
- The checker uses Python's `html.parser`, which lowercases attribute names, so React's `onClick` is read as `onclick`. It also recognises Vue's `@click` and `v-on:click`, Svelte's `on:click` and Angular's `(click)`.
- JSX and template syntax can confuse an HTML parser. Prefer the rendered HTML. When you must check templates, treat results as a first pass.
- Svelte and Vue components keep their directives when an element changes. Keep bindings, keys and component boundaries intact.
### axe-core
Deque's rule `button-name` (axe-core 4.11) checks that buttons and elements with `role="button"` have an accessible name. It maps to WCAG 4.1.2 Name, Role, Value (Level A) and is rated critical impact. Fixes it accepts: inner text, a non-empty `aria-label`, or `aria-labelledby` pointing at text. Input buttons are covered by a separate rule, `input-button-name`.
### WHATWG outline algorithm
The HTML spec replaced its document outline algorithm with one based on heading levels in 2022 (WHATWG pull request 7829, reported by Bruce Lawson on 7 July 2022). No browser had implemented the old algorithm, so nesting an `h1` in `section` never changed its level.
### Claude Agent Skills
Anthropic limits `name` to 64 characters and `description` to 1,024. Claude Code documents `disable-model-invocation: true` for skills with side effects, so they run only when the user calls them. This skill edits files, so that setting suits teams that want manual control. Install skills only from sources you trust.
## Sources
Last verified 23 October 2026 for every source below, except where a date says otherwise.
- WebAIM, The WebAIM Million (2026 report, February 2026 data): https://webaim.org/projects/million/. Failure rates and the ARIA error correlation.
- W3C WAI, Page Structure tutorial: https://www.w3.org/WAI/tutorials/page-structure/. Regions, labels, headings and WCAG mapping.
- W3C WAI, Forms tutorial (updated 27 March 2026): https://www.w3.org/WAI/tutorials/forms/. Used for the handoff boundary.
- Deque University, axe rule button-name (axe-core 4.11): https://dequeuniversity.com/rules/axe/4.11/button-name.
- Bruce Lawson, Why the HTML outlining algorithm was removed from the spec (7 July 2022): https://brucelawson.co.uk/2022/why-the-html-outlining-algorithm-was-removed-from-the-spec-the-truth-will-shock-you/.
- Ben Myers, post on clickable divs (30 September 2019): https://benmyers.dev/blog/clickable-divs/. Everything a clickable `div` needs.
- GrahamTheDev, They...just...won't...learn (27 May 2021, edited 17 December 2021): https://dev.to/grahamthedev/they-just-won-t-learn-a-button-is-a-lt-button-gt-accessibility-rants-k1b. The URL-change rule and why developers reach for `div`s.
- joabgonzalez, ai-agents-skills, html skill (via skills.cat): https://skills.cat/skills/joabgonzalez/ai-agents-skills/html. Rival implementation.
- stolinski, s-stack, html skill (via skillselion): https://skillselion.com/skills/stolinski/s-stack/html. Rival implementation.
- 5dlabs, cto-agents, web-design-guidelines (via skills.sh): https://www.skills.sh/5dlabs/cto-agents/web-design-guidelines. Rival listing, rules not visible.
- Anthropic, Agent Skills best practices (read 22 October 2026): https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices.
- Claude Code documentation, Skills (read 22 October 2026): https://code.claude.com/docs/en/skills.
---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/semantic-html/
Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
Workflow
Step 0: Confirm scope and safety
- Confirm inputs 1 to 7.
- Confirm the branch. Record the commit the work starts from.
- Tell the user what will happen: check, triage, decisions, a change plan, approved batches, verification, report.
- Gate 1: wait for a yes.
Step 1: Run the checker
- Save
scripts/semantic_check.pyfrom this skill folder. It uses only the Python standard library, reads files and writes nothing. - Run it on the rendered HTML:
python3 scripts/semantic_check.py page.html. - If only templates are available, run it on them and add "templates only, rendered output not checked" to the report. Template syntax can hide or invent elements.
- Keep the output. It is the baseline for step 7.
The checker reports these codes. Each line gives the file, line number, level and code.
| Code | Level | Meaning |
|---|---|---|
click-on-non-interactive | error | A div, span, li, td, p, img, section or article has onclick, @click, v-on:click, (click) or on:click |
link-used-as-button | error | href is empty, # or javascript: |
link-without-href | error | <a> with no href cannot take keyboard focus |
button-without-name, a-without-name | error | No text, image alt, aria-label, aria-labelledby or title |
nested-interactive | error | A link or button inside another link or button |
img-missing-alt | error | <img> with no alt attribute at all |
heading-skip | error | A heading jumps down more than one level |
main-count | error | Not exactly one main landmark |
missing-lang | error | <html> has no lang |
no-h1 | warn | No h1 on the page |
unlabelled-nav | warn | More than one nav and at least one has no label |
role-on-non-interactive | warn | role="button" or role="link" on a non-interactive element |
positive-tabindex | warn | tabindex above 0 changes the tab order |
The checker cannot judge meaning. It does not know whether a heading describes its section or whether alt text is useful. Steps 3 and 7 cover that.
Step 2: Triage
Sort every flag into one of four priority groups. Fix in this order.
| Priority | Codes | Why first |
|---|---|---|
| P1: controls | click-on-non-interactive, link-used-as-button, link-without-href, *-without-name, nested-interactive | Keyboard and screen reader users cannot reach or understand the control |
| P2: page structure | main-count, missing-lang, heading-skip, no-h1, unlabelled-nav | Users cannot move around the page or hear it in the right language |
| P3: images | img-missing-alt | Content or function is missing for screen reader users |
| P4: order | positive-tabindex, role-on-non-interactive | Behaviour is unexpected but usually still usable |
Form fields go to html-forms.md. List them in the report and do not fix them here.
Step 3: Decide the element
Use these rules. They are deterministic: the same situation always gets the same element.
Controls.
| Situation | Element |
|---|---|
| It changes the URL or loads another page or file | <a href="real-url"> |
| It does something on this page (opens a dialog, adds to basket, toggles a panel) | <button type="button"> |
| It submits a form | <button type="submit"> |
| It shows and hides a section of content with no script needed | <details> and <summary> |
| It must look like a link but performs an action | <button type="button"> styled as a link |
| It must look like a button but goes to another page | <a href> styled as a button |
The test is "does the URL change?" A link changes the URL. A button does not. Never put a button inside a link, or a link inside a button: pick one element from the table.
Landmarks.
| Region | Element | Rule |
|---|---|---|
| Main content | <main> | Exactly one per page |
| Site header | <header> | Top level of body, not inside main or article |
| Site footer | <footer> | Top level of body |
| Navigation blocks | <nav> | If there is more than one, give each a distinct aria-label such as "Main" and "Legal" |
| Related content beside the main content | <aside> | Only for content that makes sense apart from the main content |
| A self-contained item (post, product card that stands alone, comment) | <article> | It would make sense if lifted on its own |
| A themed group with its own heading | <section> | Only when it has a heading. Otherwise use <div> |
Headings.
- One
h1per page, naming what the page is about (default rule). - Never skip a level going down:
h2followsh1,h3followsh2. Going back up any number of levels is fine. - Choose the level by the section's place in the outline, never by font size. Change the size in CSS.
- A heading must describe the content beneath it. A styled sentence that introduces no section is a paragraph.
Do not rely on sectioning elements to change heading levels. The WHATWG spec replaced its outline algorithm in 2022 because no browser implemented it, so an h1 inside a section is still read as an h1.
Images.
| Image role | alt |
|---|---|
| Carries information | Describe the information, not the picture: "Sales doubled between 2024 and 2025" |
| Decorative | alt="", so screen readers skip it |
| The only content of a link or button | Describe the action or destination: "Close", "Home" |
| A complex chart or diagram | A short alt, with the full description or data in nearby text |
Never remove the alt attribute to mark an image as decorative. A missing alt and an empty alt behave differently.
Lists and tables.
- Items of the same kind in a row or column (menu links, features, steps) go in
ulorol. A card grid does not need to be a list unless the count or order helps the user. - A table holds data with rows and columns. Use
thwithscope="col"orscope="row". If a table is only for layout, replace it with CSS.
ARIA.
- If a native element does the job, use it and add no role.
- Add ARIA only for patterns with no native element (tabs, a combobox), and then the element must also get the keyboard behaviour the role promises.
aria-labelon a button or link is for an icon-only control. Do not use it to override visible text.
Step 4: Write the change plan
For every flag, write one row: file and line, current element, new element, reason (a rule from step 3), and dependants found in step 5. Group the rows into batches of at most 10 files or 30 changes.
Step 5: Check dependants before each swap
Search the codebase for every tag and class involved. Use the commands in the Templates section.
| Dependant | What breaks | What to do |
|---|---|---|
CSS selectors by tag (div.card, nav > div) | Styling disappears | Update the selector in the same batch |
| Button default styles | The new button shows a grey box | Add the button reset from the Templates section |
Scripts that query by tag or check tagName | Handlers stop firing | Update the selector. Keep the class names |
| Event handlers on the old element | Double handling, or Enter fires twice | Remove the manual keyboard handler when moving to a native button |
<button> inside a <form> | It submits the form | Always set type="button" unless it submits |
| Tests that select by tag or text | Tests fail | Update selectors. Prefer selecting by role and name |
| Tag manager triggers (click classes, element selectors) | Analytics events stop | List them for the user. The agent does not change tag manager settings |
If a dependant sits outside the files in scope, stop and ask the user before changing it.
Step 6: Apply one batch at a time
- Show the diff for the batch.
- Gate 2: wait for a yes for this batch.
- Apply the batch on the branch.
- Run step 7 before the next batch.
Step 7: Verify
- Checker. Re-run
semantic_check.py. Every flag fixed in the batch must be gone, and no new error may appear. - Keyboard. Using only the keyboard, Tab through the changed area. Every control receives focus in reading order with a visible focus indicator. Enter activates links. Enter and Space activate buttons.
- Screen reader spot check. Open the headings list and the landmarks list in a screen reader (the rotor in VoiceOver, the elements list in NVDA). Headings form a sensible outline. There is one main region and each navigation has a distinct name.
- Automated rules. Run axe or the team's equivalent on the changed pages. Zero new violations.
- Visual. Compare screenshots before and after. Layout must not shift unless the user approved it.
- Tests. The project's existing tests pass.
Automated checks find only a share of problems. The WebAIM Million 2026 report counted detectable failures on 95.9% of one million home pages and put full conformance at below 4.1%, because only automatically detectable failures were counted. Steps 7.2 and 7.3 are not optional.
Step 8: Report
Use the report template. State what changed, what is left, what was handed to html-forms.md, and what was not checked.
Worked example
The page below is invented. The checker output is real: it was produced by running the script in this skill on these files.
Input. A pricing page, plain HTML, under version control. No CSS targets div.card by tag. A tag manager trigger listens for clicks on .card.
<!doctype html>
<html>
<head><title>Plans</title></head>
<body>
<div class="top">
<nav><a href="/">Home</a> <a href="/pricing">Pricing</a></nav>
</div>
<div class="content">
<h1>Choose a plan</h1>
<h3>Monthly</h3>
<div class="card" onclick="selectPlan('pro')">Pro plan</div>
<a href="#" onclick="openChat()">Talk to sales</a>
<button class="icon-close"><svg aria-hidden="true"></svg></button>
<img src="/badge.png">
<a href="/terms"><button>Read terms</button></a>
</div>
<div class="footer"><nav><a href="/privacy">Privacy</a></nav></div>
</body>
</html>Step 1, checker output (tabs shown as spaces):
before.html:1 error main-count 0 main landmarks; expected 1
before.html:1 error missing-lang <html> has no lang
before.html:1 warn unlabelled-nav 2 nav elements, not all labelled
before.html:10 error heading-skip h1 followed by h3
before.html:11 error click-on-non-interactive <div> has a click handler; use <button> or <a href>
before.html:12 error link-used-as-button href="#"; actions belong in <button>
before.html:13 error button-without-name no text, alt or aria-label
before.html:14 error img-missing-alt /badge.png
before.html:15 error nested-interactive <button> inside another link or button
before.html: 8 errors, 1 warningsSteps 2 and 3, decisions.
| Line | Decision | Rule |
|---|---|---|
| 11 | <button type="button" class="card"> | Selecting a plan stays on the page |
| 12 | <button type="button"> styled as a link | Opening chat does not change the URL |
| 13 | aria-label="Close" | Icon-only control |
| 15 | Keep the link, drop the inner button | "Read terms" goes to another page |
| 10 | h3 becomes h2 | No skipped levels |
| 14 | alt="" | The user confirms the badge is decorative |
| Structure | header, main, footer; lang="en-GB"; navs labelled "Main" and "Legal" | Landmark rules |
Step 5, dependants. The .card class stays, so the tag manager trigger keeps firing. The agent lists it anyway for the user to confirm. The new buttons get the reset styles. selectPlan and openChat move from inline attributes to listeners on data-plan and data-action, so the agent shows that script change in the same batch.
After (one batch, approved):
<!doctype html>
<html lang="en-GB">
<head><title>Plans</title></head>
<body>
<header>
<nav aria-label="Main"><a href="/">Home</a> <a href="/pricing">Pricing</a></nav>
</header>
<main>
<h1>Choose a plan</h1>
<h2>Monthly</h2>
<button type="button" class="card" data-plan="pro">Pro plan</button>
<button type="button" class="link-style" data-action="chat">Talk to sales</button>
<button type="button" class="icon-close" aria-label="Close"><svg aria-hidden="true"></svg></button>
<img src="/badge.png" alt="">
<a href="/terms">Read terms</a>
</main>
<footer><nav aria-label="Legal"><a href="/privacy">Privacy</a></nav></footer>
</body>
</html>Step 7, checker output: after.html: 0 errors, 0 warnings. The user then runs the keyboard and screen reader checks and confirms them. The report lists one item for the user: confirm the .card click trigger still fires in the tag manager preview.
Templates
Checker
scripts/semantic_check.py ships with this skill. Usage:
python3 scripts/semantic_check.py page.html other.htmlIt exits with status 1 if any error is found, so it can run in CI. Read it before running it.
Finding dependants
grep -rnE "div\.card|\.card\b" --include=*.css --include=*.scss src/
grep -rnE "querySelector(All)?\(['\"][^'\"]*card|tagName" --include=*.js --include=*.ts src/
grep -rnE "getByText|querySelector|\.card" --include=*.test.* --include=*.spec.* .Replace card with the class or tag being changed.
Button reset
button.link-style,
button.card {
appearance: none;
background: none;
border: 0;
padding: 0;
font: inherit;
color: inherit;
text-align: inherit;
cursor: pointer;
}
button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }Keyboard check
[ ] Tab reaches every control in the changed area, in reading order
[ ] Focus is visible on every control
[ ] Enter follows every link
[ ] Enter and Space activate every button
[ ] No control is reached that should not be (hidden, decorative)
[ ] Shift+Tab moves back in the same orderReport
SEMANTIC HTML REPORT: [files], [date], branch [name] from [commit]
Baseline: [n] errors, [n] warnings
Fixed this run: [n] (P1 [n], P2 [n], P3 [n], P4 [n])
Remaining: [list with reason]
Handed to html-forms: [n] form fields
Dependants changed: [CSS, scripts, tests]
Dependants for the user: [tag manager triggers, files out of scope]
Verification: checker [0 errors], keyboard [pass/fail], screen reader [pass/fail],
automated rules [n new violations], visual [no shift], tests [pass]
Not checked: [rendered output, colour contrast, focus styles beyond changed area]
This report is not a statement of WCAG or legal conformance.Starting prompt
Use the semantic-html skill on [files]. The stack is [stack]. Work on the
branch [name]. Start with the checker and a change plan, and do not edit
anything until I approve a batch.Failure modes and anti-patterns
| # | Failure | How to spot it | What to do instead |
|---|---|---|---|
| 1 | ARIA patched onto a div | role="button" and tabindex="0" added, but no Enter or Space handling and no disabled state | Replace with <button type="button"> and a style reset. A native button brings focus, keyboard activation and form behaviour |
| 2 | Heading levels picked for size | h4 used because it "looks right"; the checker flags heading-skip | Pick the level from the outline. Move size into CSS classes |
| 3 | A swap breaks styling, scripts or tracking | Visual diff shows a grey box; a click handler stops; an analytics event stops | Run step 5 before each swap. Keep class names. List tag manager triggers for the user |
| 4 | An automated pass treated as done | The report says "0 errors" and nobody used a keyboard | Run steps 7.2 and 7.3 every time. The checker and axe catch only what can be detected by rule |
| 5 | Links and buttons mixed up | A button inside a link, href="#" for actions, a <button> that navigates with script | Ask "does the URL change?" and use the table in step 3 |
Quality bar
Check every item before handing over.
- The user approved gate 1, and the work is on a named branch.
- The baseline checker output is saved and quoted in the report.
- Every flag is in a priority group, fixed, handed to
html-forms.md, or listed as remaining with a reason. - Every element decision cites a rule from step 3.
- Every batch had at most 10 files or 30 changes and was approved before it was applied.
- Every
<button>added has an explicittype. - Dependants were searched for each changed tag and class, and the result is in the report.
- The checker shows zero errors in the changed files, with no new errors elsewhere.
- The keyboard check was run on the changed area and its result is recorded.
- No
rolewas added where a native element exists. - No page text, hidden input value or inline script content appears in the report.
- The report says it is not a conformance statement.
Cost and effort
The checker runs locally and makes no network calls. The agent reads its output and only the lines it flags, so token use grows with the number of problems, not the size of the page. The cheaper path for a large site is to check templates or shared components first, because one fix there repairs every page that uses them.
Handoffs to related skills
html-forms.md handles every form field the checker or triage surfaces. core-web-vitals.md and javascript-seo.md cover performance and rendering for search. structured-data.md covers schema markup. ai-evals-testing.md helps if the team wants to test an agent's markup output at scale.
Appendix: tool-specific notes
Every fact below was read on 23 October 2026. Verify before relying on this: tools and rules change.
Frameworks
- The checker uses Python's
html.parser, which lowercases attribute names, so React'sonClickis read asonclick. It also recognises Vue's@clickandv-on:click, Svelte'son:clickand Angular's(click). - JSX and template syntax can confuse an HTML parser. Prefer the rendered HTML. When you must check templates, treat results as a first pass.
- Svelte and Vue components keep their directives when an element changes. Keep bindings, keys and component boundaries intact.
axe-core
Deque's rule button-name (axe-core 4.11) checks that buttons and elements with role="button" have an accessible name. It maps to WCAG 4.1.2 Name, Role, Value (Level A) and is rated critical impact. Fixes it accepts: inner text, a non-empty aria-label, or aria-labelledby pointing at text. Input buttons are covered by a separate rule, input-button-name.
WHATWG outline algorithm
The HTML spec replaced its document outline algorithm with one based on heading levels in 2022 (WHATWG pull request 7829, reported by Bruce Lawson on 7 July 2022). No browser had implemented the old algorithm, so nesting an h1 in section never changed its level.
Claude Agent Skills
Anthropic limits name to 64 characters and description to 1,024. Claude Code documents disable-model-invocation: true for skills with side effects, so they run only when the user calls them. This skill edits files, so that setting suits teams that want manual control. Install skills only from sources you trust.
Sources
Last verified 23 October 2026 for every source below, except where a date says otherwise.
- WebAIM, The WebAIM Million (2026 report, February 2026 data): https://webaim.org/projects/million/. Failure rates and the ARIA error correlation.
- W3C WAI, Page Structure tutorial: https://www.w3.org/WAI/tutorials/page-structure/. Regions, labels, headings and WCAG mapping.
- W3C WAI, Forms tutorial (updated 27 March 2026): https://www.w3.org/WAI/tutorials/forms/. Used for the handoff boundary.
- Deque University, axe rule button-name (axe-core 4.11): https://dequeuniversity.com/rules/axe/4.11/button-name.
- Bruce Lawson, Why the HTML outlining algorithm was removed from the spec (7 July 2022): https://brucelawson.co.uk/2022/why-the-html-outlining-algorithm-was-removed-from-the-spec-the-truth-will-shock-you/.
- Ben Myers, post on clickable divs (30 September 2019): https://benmyers.dev/blog/clickable-divs/. Everything a clickable
divneeds. - GrahamTheDev, They...just...won't...learn (27 May 2021, edited 17 December 2021): https://dev.to/grahamthedev/they-just-won-t-learn-a-button-is-a-lt-button-gt-accessibility-rants-k1b. The URL-change rule and why developers reach for
divs. - joabgonzalez, ai-agents-skills, html skill (via skills.cat): https://skills.cat/skills/joabgonzalez/ai-agents-skills/html. Rival implementation.
- stolinski, s-stack, html skill (via skillselion): https://skillselion.com/skills/stolinski/s-stack/html. Rival implementation.
- 5dlabs, cto-agents, web-design-guidelines (via skills.sh): https://www.skills.sh/5dlabs/cto-agents/web-design-guidelines. Rival listing, rules not visible.
- Anthropic, Agent Skills best practices (read 22 October 2026): https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices.
- Claude Code documentation, Skills (read 22 October 2026): https://code.claude.com/docs/en/skills.
What this skill does
Most markup problems pass code review because they work with a mouse. A div with a click handler, a link with href="#" and a heading chosen for its size all look fine until someone uses a keyboard or a screen reader.
This skill starts with a short Python checker that reads HTML and reports 14 problem codes with line numbers. It sorts them into four priority groups, picks the element for each one from decision tables, and checks every CSS selector, script, test and tag manager trigger that targets the element before it changes anything. Changes go in batches of at most 10 files or 30 element changes, each shown as a diff and applied only on your yes. Verification is the checker again, a keyboard pass and a screen reader spot check.
I read three published HTML skills before writing this one. Two give good writing guidance but no audit procedure and no automated check, and the third is a listing whose rules are not visible. None of them protects the styling, scripts and tracking that depend on the markup.
Who it is for
Use it if you maintain a site or component library and want an agent to clean up old markup, review pull requests, or check what an AI coding tool generated. It suits frontend developers, agencies inheriting someone else’s templates, and marketers who own a CMS theme. It works on plain HTML and on the rendered output of React, Vue, Svelte or Angular, because the checker reads the HTML the browser receives, and it recognises the click attributes each framework uses.
It is the wrong skill for form labels and validation (use HTML Forms), for colour contrast or a full WCAG audit, or for search metadata. It does not issue conformance statements.
What is inside
The file is 4,316 words (version 1.0.0), with a checker script in scripts/, and holds:
- Eight guardrails, including propose-then-change, small batches, native elements before ARIA and a not-legal-advice line.
- Seven inputs the agent must have first, with the exact question for each.
- An eight-step workflow with decision tables for links and buttons, landmarks, headings, images, lists, tables and ARIA.
- A dependants check with grep commands for CSS, scripts and tests.
- A standard-library Python checker in
scripts/semantic_check.pythat exits with an error code for CI. - A worked example with an invented pricing page, run through the checker before and after.
- A button reset, a keyboard checklist, a report template and a starting prompt.
- Five failure modes, a 12-item quality bar and a fenced appendix on frameworks and axe-core.
How to install and use it in Claude, Cursor and other agents
Claude. Keep the folder as semantic-html/ with SKILL.md and scripts/semantic_check.py inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ for personal use or .claude/skills/ in a project.
Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the script beside your code. Then send the starting prompt from the Templates section.
Before you install. Read the checker first. It reads files and writes nothing. Install skills only from sources you trust, and check your tool’s documentation for current folder paths.
A worked example
An invented pricing page has no main landmark, no lang, a skipped heading, a clickable div, a link used to open chat, an icon button with no name, an image with no alt and a button inside a link. The checker reports 8 errors and 1 warning.
The agent turns the div and the chat link into buttons because neither changes the URL, keeps the terms link and drops the button inside it, fixes the heading, and adds landmarks with labelled navigation. It keeps the class names so a tag manager trigger still fires. After one approved batch, the checker reports 0 errors and 0 warnings.
Common mistakes
The skill is built around five ways this goes wrong:
- Patching a div with ARIA. A role without keyboard handling still fails. The skill swaps in a native button.
- Picking headings by size. The level comes from the outline, and size moves to CSS.
- Breaking what depends on the markup. Every swap is checked against CSS, scripts, tests and tracking first.
- Trusting an automated pass. The WebAIM Million 2026 report puts full conformance below 4.1% because tools only see what they can detect, so the skill always adds a keyboard and screen reader check.
- Mixing up links and buttons. One question decides it: does the URL change?
Related skills
HTML Forms handles every form field this skill surfaces. Core Web Vitals and JavaScript SEO cover performance and rendering. Structured Data covers schema. Browse the HTML and Semantics set.
Frequently asked questions
What is semantic HTML?
Semantic HTML uses the element that matches what content is or does: a button for an action, a link for navigation, headings for sections, and landmarks such as main and nav for page regions. Browsers and screen readers read that meaning, so keyboard access and navigation work without extra code. This skill audits existing markup against those rules and fixes it in approved batches.
Should I use a button or a link?
Ask whether the URL changes. If it does, use a link with a real href. If the control does something on the current page, such as opening a dialog or adding to a basket, use a button with type set to button. Never put one inside the other. This skill applies that rule to every control it finds.
Can a page have more than one h1?
The HTML spec dropped its outline algorithm in 2022 because no browser had implemented it, so an h1 inside a section is still read as a top-level heading. This skill uses one h1 per page as its default rule, picks every other level from the page outline rather than font size, and flags any heading that skips a level going down.
Is a div with role button accessible?
Only if you also add keyboard focus, Enter and Space handling, a disabled state and focus styles, which is fragile work a native button gives you for free. This skill does not add roles where a native element exists. It replaces the div with a button and adds a small CSS reset so the design stays the same.
Can AI fix accessibility issues in my HTML?
An agent can find and fix many markup problems quickly, and this skill gives it a checker, decision rules and a verification routine. It cannot certify that a site meets WCAG or any law. Automated checks catch only part of the problem, so the skill always ends with a keyboard pass and a screen reader spot check by a person.
Will the agent change my code without asking?
No. The skill reports and plans first, works on a branch, and changes files in batches of at most 10 files or 30 element changes. Each batch is shown as a diff and applied only after you approve it. It never pushes, merges or deploys, and it stops to ask if a fix would touch files outside the agreed scope.
Changelog
- 1.0.0, 23 October 2026: first release.
Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.
All HTML and Semantics skills
