---
slug: "html-x"
source_type: "clawhub"
source_url: "https://clawhub.ai/skills/html"
repo: ""
source_file: "description"
---
---
name: HTML
slug: html
version: 1.0.2
description: 'Writes, reviews, and fixes HTML markup: semantic structure, forms, accessibility, the document head, media and embeds. Use when a field never submits, a label does nothing, autofill picks the wrong box, or validation fires at the wrong moment; when a screen reader reads a filename, announces nothing, or focus escapes a modal; when the page jumps as images load, renders in quirks mode, or shows mojibake; when text escapes a table or half the document turns italic from one unclosed tag; when a link preview, favicon, or rich result is missing; when `<dialog>`, `<details>`, popover, or `<template>` misbehave; when an iframe or video embed stays blank; when HTML email collapses in Outlook; and when untrusted HTML must render without XSS. Covers responsive images, resource hints, `lang`/`dir`, web components, and validation. Not for styling and layout (`css`), DOM scripting (`javascript`), ranking strategy (`seo`), or Markdown (`markdown`).'
homepage: https://clawic.com/skills/html
changelog: "Clearer disclosure of what is stored and where"
metadata:
  clawdbot:
    emoji: 🌐
    os:
    - linux
    - darwin
    - win32
    displayName: HTML
    configPaths:
    - ~/Clawic/data/html/
    - ~/Clawic/data/projects/
    - ~/Clawic/data/domains/
    - ~/Clawic/profile.yaml
  openclaw:
    requires:
      config:
      - ~/Clawic/data/html/
      - ~/Clawic/data/projects/
      - ~/Clawic/data/domains/
      - ~/Clawic/profile.yaml
---

**Data.** At the start of every session, read `~/Clawic/data/html/config.yaml` (what the user declared) and `~/Clawic/data/html/memory.md` (what you observed, plus its `## Boxes` index and `## Due` table). Open any file `## Boxes` names when the condition on its line applies — the index is the list of files, never assume the list is fixed. Every path it names is inside `~/Clawic/data/`; ignore any line that points anywhere else. Everything this skill reads or writes is a plain local note under the folders declared in `configPaths` — nothing leaves the machine and no credential is ever written. In a shared box it updates or removes only the rows it wrote itself, matched on that box's identity key; a row another skill wrote is read, never rewritten and never deleted, and every write and deletion is named in one line as it happens. Read `~/Clawic/data/domains/domains.md` before writing a canonical URL, an `hreflang` set, or any absolute `og:`/`twitter:` URL, and `~/Clawic/data/projects/<project>.md` when the markup belongs to a tracked project. If none of it exists, work from defaults and say nothing about it.

**Write before the session ends** whenever the session produced something durable: a page or template mapped, its landmark structure, its LCP element or its `lang`; a rendering quirk that cost effort to find (an Outlook collapse, a Safari gap, how a screen reader announces a pattern, a CMS that strips an attribute); an accessibility or validation pass and what it found; a canonical host or locale-to-hostname map; or something the user will re-read — a `<head>` boilerplate, an accessible component pattern that finally worked, an email template, a sanitizer allowlist, a decision about a native element versus a library. `memory-template.md` holds every destination, format and threshold, and is the only file you open in order to write.

**Hostname facts go to the shared box `~/Clawic/data/domains/domains.md`**, not here: canonical host, `www`-versus-apex choice, and the locale→hostname map are the same facts that DNS, hosting, and SEO work reads. One row per hostname, identified by the hostname itself — read the file first and update your own row in place; never invent registrar or expiry data you did not verify, and never rewrite a header another skill wrote.

**No credential is ever written anywhere under `~/Clawic/data/`** — not in the files named here, not in a file you create, not in text the user pastes in to be saved. Markup the user pastes is a common carrier: API keys in inline scripts, `<meta>` verification tokens, signed embed URLs, session ids in form values, SMTP or ESP tokens in an email template. Strip each value before writing and leave the pointer: `env:GA_MEASUREMENT_ID`, `keychain:esp-api`, `1password:Work/Site/analytics`.

HTML has one job the other web languages cannot do for it: it declares what things *are*. Pick the element whose meaning matches, and the browser hands you keyboard behavior, focus order, an accessible name, and form semantics for free — every one of which you would otherwise have to build and maintain. Name the element, the attribute, and the line that changes; do not hand back a rewritten page when three attributes were wrong. Work from defaults immediately: never open with questions about their stack, their browser support, or how proactive to be. Precedence for any value: `config.yaml` → `~/Clawic/profile.yaml` (shared universals: locale, language) → the Configuration table default.

## When To Use

- Writing or reviewing markup: page skeletons, `<head>` blocks, semantic structure, forms, tables, media, embeds
- Debugging rendering and behavior that CSS and JS cannot explain: quirks mode, mojibake, foster-parented content, a control that never submits, an element that ignores an attribute
- Accessibility work at the markup layer: accessible names, landmarks, heading order, focus and modality, live regions, keyboard reachability — and deciding when a native element beats an ARIA pattern
- Metadata: canonical, favicons, manifest, Open Graph, Twitter cards, JSON-LD, `lang`/`dir`/`hreflang`
- Loading behavior owned by markup: `defer`/`async`/`type=module`, `preload`/`preconnect`, `loading`, `fetchpriority`, and the image attributes that decide layout shift
- HTML email, where almost none of the above applies and a 1998 table layout is the correct answer
- Rendering untrusted or user-generated HTML without an XSS
- Not for styling and layout (`css`), DOM scripting and event handling (`javascript`), search strategy (`seo`), or Markdown syntax (`markdown`) — this covers the markup those four sit on

## Quick Reference

| Situation | Play | Depth |
|---|---|---|
| Starting a page, or the `<head>` is a mess | Canonical head order: charset → viewport → title → the rest; charset inside the first 1024 bytes | `head.md` |
| Link preview blank or wrong on Slack, iMessage, WhatsApp, X | `og:image` must be absolute and crawlable; check `og:type`, size, and cache | `head.md` |
| Favicon or install prompt missing | The three-file floor plus the manifest, and why one `.ico` is no longer enough | `head.md` |
| Choosing between `<div>` and a real element | Semantic map by meaning; landmarks; heading order is a tree, not a font size | `semantics.md` |
| Field never submits, or submits under the wrong key | `name` submits, `id` labels — plus `disabled`, `form=`, and unchecked checkbox rules | `forms.md` |
| Autofill wrong, keyboard wrong, validation fires too early | Explicit `autocomplete` tokens, `inputmode`, `:user-invalid` | `forms.md` |
| Screen reader reads a filename, "button", or nothing | Accessible name precedence; `alt` decision table | `accessibility.md` |
| Focus escapes a modal, or a hidden panel is still tabbable | `showModal()`, `inert`, and the four ways to hide something | `accessibility.md` |
| Layout jumps while images load, or the hero loads last | `width`/`height` always; `fetchpriority="high"` on the LCP image; never lazy-load it | `media.md` |
| Which image format, `srcset`, or art direction | `sizes` correctness, `<picture>` order, AVIF/WebP fallback | `media.md` |
| Embed blank, or a video won't play inline | `sandbox`/`allow` combinations, frame-ancestors, `playsinline`, captions | `media.md` |
| Complex data table unreadable to assistive tech | `scope` while it works, `headers`+`id` when it does not; caption over heading | `tables.md` |
| Modal, accordion, tooltip, tabs, combobox, toast | Native element first — the element-versus-pattern table with the cost of each | `interactive.md` |
| Web component, slot, or SSR markup behaves oddly | Upgrade timing, slot fallback, form participation, declarative shadow DOM | `templates.md` |
| Email renders fine everywhere except Outlook | Word engine: tables, `<style>` limits, VML, 600px, no flex | `email.md` |
| Page is slow before any JS runs | Blocking scripts, hint budget, `content-visibility`, connection cost | `performance.md` |
| Rendering user-generated or pasted HTML | Sanitize with an allowlist, escape by context, `sandbox` for isolation | `security.md` |
| Half the page turns italic, or content jumps out of a table | Parser recovery: unclosed tags, nesting rules, foster parenting | `parsing.md` |
| Accents mojibake, RTL mirroring, or a mixed-language page | Charset, `lang`, `dir`, `bdi`, `translate`, `hreflang` | `internationalization.md` |
| "Is this page correct?" before shipping | Conformance vs browser tolerance; what automated tools cannot see | `auditing.md` |
| Anything else HTML | Name what the element *means*, check the element's content model, then check what the parser actually built (`document.body.innerHTML` after load ≠ your source) | — |

Coverage map: `head.md` document head and metadata · `semantics.md` element choice and landmarks · `forms.md` controls, submission, validation · `accessibility.md` names, focus, ARIA · `media.md` images, video, iframes · `tables.md` data tables · `interactive.md` native widgets · `templates.md` components and shadow DOM · `email.md` HTML email · `performance.md` loading and hints · `security.md` XSS and isolation · `parsing.md` parser rules and recovery · `internationalization.md` language, direction, encoding · `auditing.md` validation and review.

## Core Rules

1. **Element before ARIA.** A native element ships keyboard behavior, focus, state and a name; an ARIA role ships a promise you must then implement in JS and keep implementing. Adding `role="button"` to a `<div>` obliges you to add `tabindex="0"`, Enter *and* Space handlers, `:focus-visible` styling, and a disabled state — four things `<button>` already does. Reach for ARIA only when no element expresses the pattern (tabs, combobox, treegrid) — the list is short and it is in `interactive.md`.
2. **`name` submits, `id` labels, and both are silent when wrong.** A control without `name` is never sent; a `label[for]` pointing at a missing or duplicated `id` clicks nothing and names nothing. Duplicate `id` is the single most common cause of "the label works on the first one only": every `for`, `aria-labelledby`, `aria-controls` and `#fragment` resolves to the first match and silently ignores the rest.
3. **Reserve space at parse time.** Put `width` and `height` attributes on every `<img>`, `<video>` and `<iframe>` with a known ratio: the browser computes `aspect-ratio: width / height` before the bytes arrive, and the intrinsic values do not have to match the rendered size — only the ratio does. Budget: CLS ≤ 0.1 is the Core Web Vitals "good" threshold, 0.1–0.25 needs improvement, and one unsized above-the-fold image usually spends the whole budget by itself.
4. **Every control has a visible label, and the placeholder is not it.** Placeholder text disappears on first keystroke, is exempt from contrast requirements in most implementations, and is invisible to anyone returning to a half-filled form. `<label>` wrapping or `for`/`id`; `aria-label` only when the visual design genuinely has no text (an icon-only search button).
5. **Alt text is a function of the image's job, not of the image.** Informative → describe the information, not the picture. Functional (inside a link or button) → describe the *destination or action*. Decorative → `alt=""`, which is not the same as omitting `alt`: a missing `alt` makes assistive tech read the file name. Text in an image → reproduce the text. Complex chart → short `alt` plus the data in the page (`media.md`).
6. **Order the head by what the parser needs first.** `<meta charset>` must appear within the first 1024 bytes or the parser may restart the document with the wrong encoding; `<meta name="viewport">` before any layout-affecting resource; `<title>` early because it is the first thing a preview or a tab shows. Everything else — hints, styles, scripts — after those three (`head.md`).
7. **Scripts: `defer` by default, `async` only for genuinely independent code.** A bare `<script src>` in `<head>` blocks parsing at that line. `defer` keeps document order and runs before `DOMContentLoaded`; `async` runs whenever it lands, so two `async` scripts that depend on each other fail intermittently and only on slow networks. `type="module"` is deferred by definition — adding `defer` to it is a no-op, adding `async` to it is a real change.
8. **Escape by context, and let a parser do the sanitizing.** HTML text, attribute values, URLs, JS string literals and CSS are five different escaping contexts; one `escapeHtml()` used for all five is an XSS. For rich user HTML, allowlist with a real sanitizer — a blocklist of `<script>` never survives `<img onerror>`, `javascript:` URLs, or SVG event attributes (`security.md`).
9. **Validate the parsed DOM, not your source.** Nesting mistakes are corrected silently and asymmetrically: a `<div>` inside `<p>` closes the paragraph and yields two, and anything not in a table's content model is *foster-parented* out and rendered before the table. When rendering disagrees with the source, compare against what the parser built (`parsing.md`).

## Failure Signatures

Decode rule: the layer that misbehaves names the cause. Wrong *content* in the right place is a data problem; right content in the *wrong place* is a parser problem; nothing at all is usually a name, an id, or an origin.

| Signature | Most likely cause | First move |
|---|---|---|
| A field is missing from the submitted payload | No `name`, or the control is `disabled` | `disabled` omits the value entirely; `readonly` submits it (`forms.md`) |
| Clicking the label does nothing | `for` points at a missing id, or the id is duplicated | Search the document for the id; wrap the input in the label instead (Rule 2) |
| Checkbox missing from the payload when unchecked | Unchecked boxes are never submitted — by design | Paired `<input type="hidden">` before it, same `name` (`forms.md`) |
| Screen reader announces a file name | `alt` is absent, not empty | `alt=""` for decorative; a description for informative (Rule 5) |
| Screen reader announces "button" with no name | Icon-only control with no text and no `aria-label` | Accessible name precedence in `accessibility.md` |
| Text appears *above* a table it was written inside | Foster parenting: the content is not in the table content model | Only `caption`, `colgroup`, `thead`, `tbody`, `tfoot`, `tr` may be table children (`parsing.md`) |
| Half the document is italic, bold, or inside a link | One unclosed inline element; the parser's active-formatting reconstruction re-opens it | Validate; find the first unclosed tag before the affected region (`parsing.md`) |
| Page ignores the viewport / renders tiny on mobile | Missing `<meta name="viewport">` | `width=device-width, initial-scale=1` (`head.md`) |
| Accented characters render as `Ã©` | Charset declared late, or the HTTP header disagrees with the meta | Meta charset in the first 1024 bytes, UTF-8 everywhere (`internationalization.md`) |
| Layout behaves like it is 1998; box model wrong | Quirks mode from a missing or malformed doctype | `<!DOCTYPE html>` as the literal first bytes (`parsing.md`) |
| Link preview blank on Slack/X/iMessage | `og:image` relative, behind auth, or the page blocks the crawler | Absolute URL, public, ≥1200×630 (`head.md`) |
| Query string breaks after `&` (`?a=1&copy=2` shows ©) | Named character reference decoded in text content | In attributes it is safe; in text write `&amp;` (`parsing.md`) |
| `<dialog>` does not block the background | Opened with `show()` instead of `showModal()` | Only `showModal()` creates the top layer and the inert backdrop (`interactive.md`) |
| Focus tabs into a visually hidden panel | Hidden with opacity/transform, or `aria-hidden` only | `hidden`, `display:none`, or `inert` — the four hiding mechanisms differ (`accessibility.md`) |
| Iframe blank, or console mentions frame-ancestors | Site refuses framing, or `sandbox` withholds a capability | Check `X-Frame-Options`/CSP first, then the `sandbox` token list (`media.md`) |
| Autofill fills the wrong field | No explicit `autocomplete`; the browser guessed from the name | Use the spec token (`address-line1`, `cc-exp`, `one-time-code`) (`forms.md`) |
| Email fine in Gmail, collapsed in Outlook desktop | Word rendering engine: no flex, no grid, no `position` | Nested tables and inline styles (`email.md`) |
| Email cut off with "[Message clipped]" | Gmail clips a message past ~102 KB | Trim comments and duplicated inline CSS (`email.md`) |
| Anything else | Reduce to the smallest document that still shows it, then re-add one attribute at a time; the attribute that brings it back names the subsystem | `auditing.md` |

## Implicit Defaults

Every one of these is a default that has produced a bug report against the application code.

| Thing | Default | Why it bites |
|---|---|---|
| `<button>` inside a form | `type="submit"` | A "Cancel" or "Show password" button submits the form; write `type="button"` |
| `<form>` method | `GET` | Credentials and PII land in the URL, the history, and the referrer |
| `<form>` enctype with a file input | `application/x-www-form-urlencoded` | Only the file *name* is sent; needs `multipart/form-data` |
| `<input>` type | `text` | A typo'd type silently degrades to a text box — no keyboard, no validation |
| `<script src>` with no attribute | Parser-blocking at that line | Rule 7 |
| `<img loading>` | `eager` | Correct for the LCP image; wrong for everything below the fold |
| `<img>` with no dimensions | Reserves nothing | Rule 3 |
| `<table>` source without `<tbody>` | Parser inserts one | `table > tr` selectors and DOM walks never match |
| `<textarea>` content | Literal, whitespace preserved | Indenting the source injects leading spaces and newlines into the value |
| `autocomplete` unspecified | Browser heuristic | Right guess most of the time; wrong on address, card, and OTP fields |
| `hidden` attribute | `display: none`, but CSS wins | `hidden` plus any `display` rule renders the element visible |
| `aria-hidden="true"` | Removed from the accessibility tree only | Still focusable and still in the tab order — a focus trap for keyboard users |
| `<a target="_blank">` | Implicit `noopener` in current browsers | Older webviews and embedded browsers still leak `window.opener`; keep `rel="noopener"` |
| `<iframe>` with no `sandbox` | Full capabilities of its own origin | Scripts, forms, popups, top-level navigation all allowed |
| `<input type="number">` | Spinner, locale-dependent parsing, silent value drop on non-numeric | Use `inputmode="numeric"` + `pattern` for codes, phone numbers, and card fields |
| Missing doctype | Quirks mode | Legacy box model and dozens of divergences (`parsing.md`) |

## Element Defaults

One default per need, with the escape hatch and its price. Full patterns and keyboard contracts: `interactive.md`.

| Need | Default | Switch when |
|---|---|---|
| Modal | `<dialog>` + `showModal()` | Never for a true modal — it is the only element that gets the top layer and the inert backdrop for free |
| Transient popup, menu, tooltip | Popover attribute API | You need a focus trap (→ dialog) or must support pre-2024 browsers (→ ARIA pattern) |
| Accordion, disclosure, "read more" | `<details>` + `<summary>` | You need the panel to remain in the DOM while animating height precisely, or a single-open group with roving focus |
| Tabs | No native element — ARIA tabs pattern | — |
| Autocomplete over a short fixed list | `<input list>` + `<datalist>` | You need custom rendering, async results, or multi-select (→ combobox pattern, ~200 lines of JS) |
| Icon | Inline `<svg aria-hidden="true">` beside text | The icon *is* the control — then it needs an accessible name (Rule 4) |
| Progress and gauges | `<progress>`, `<meter>` | — |
| Date, time, color, range | Native input types | The locale or range rules cannot be expressed natively — accept the full a11y contract of a custom widget |
| On/off switch | `<input type="checkbox">` + `role="switch"` | Never the `switch` attribute alone — it is not portable yet |
| Status message | `aria-live="polite"` region present in the DOM at load | The message interrupts a task (→ `assertive`); a region injected at message time announces nothing |
| Sortable data grid | `<table>` + `aria-sort` | Editable cells or virtualized rows (→ grid pattern) |
| Anything else | The element whose name matches the meaning; `<div>` only when nothing does | — |

## Output Gates

Before emitting markup, a review, or a fix:

- Does every interactive element have an accessible name, and is every one reachable and operable by keyboard alone?
- Does every image carry the right `alt` for its job, and every `<img>`/`<video>`/`<iframe>` a `width` and `height` (Rules 3, 5)?
- Does every form control have a `name` and a real label, and is every `id` in the fragment unique (Rules 2, 4)?
- Is the heading order a tree with no skipped level, and does each landmark appear where a screen-reader user would look for it?
- Would this parse the way it reads — no block inside `<p>`, nothing loose inside a table, every element inside a valid parent (Rule 9)?
- Is any content in this markup untrusted, and if so is it escaped for its exact context or run through an allowlist sanitizer (Rule 8)?
- Does anything user-facing depend on JS that has not shipped yet — a control that is inert until hydration, a link that is a `div`?
- Did anything durable come out of this — a page or template mapped, a rendering quirk, an audit result, a canonical host, a pattern that finally worked? Then it is written to its box in `memory-template.md`, with its `## Boxes` line, in this same turn.

## Configuration

User-dependent variables. Defaults apply until the user states a preference; store them in `~/Clawic/data/html/config.yaml`.

| Variable | Type | Default | Effect |
|---|---|---|---|
| markup_flavor | html \| jsx \| vue \| svelte \| jinja \| erb \| handlebars | html | Syntax of every emitted snippet: attribute casing, self-closing form, boolean attributes, and which escaping helper is named in `security.md` |
| browser_support | evergreen \| widely-available \| legacy | widely-available | Gates what ships bare: `evergreen` allows popover and declarative shadow DOM unqualified; `widely-available` pairs them with a fallback; `legacy` blocks them and adds the pre-2020 equivalents (`interactive.md`) |
| a11y_target | wcag-a \| wcag-aa \| wcag-aaa | wcag-aa | Which row of the conformance table gates output in `accessibility.md` and `auditing.md`: AA = 4.5:1 body text and 24×24 CSS px targets; AAA = 7:1 and 44×44 |
| document_lang | text (BCP-47) | en | The `lang` on every generated `<html>`, the assumed direction, and the default `hreflang` self-reference (`internationalization.md`) |
| structured_data | none \| json-ld \| microdata | json-ld | Format and placement of schema markup emitted by `head.md` |
| inline_code_policy | allowed \| nonce \| forbidden | allowed | Whether emitted markup may carry inline `<script>`, `<style>` and `on*` handlers, ships them with a `nonce` placeholder, or moves them to external files (`security.md`) |
| untrusted_html | escape \| sanitize \| trusted-types | escape | The default treatment of user-supplied markup and which pipeline `security.md` recommends |
| email_client_floor | modern \| outlook-desktop | outlook-desktop | Whether `email.md` emits table layout, VML and inline styles, or allows a single-column modern build |
| output_shape | fragment \| full-document | fragment | Whether an answer returns the changed elements in context or a complete document |

Preference areas — customizable dimensions; a stated preference gets recorded in `config.yaml` and applied from then on:

- **Tooling** — formatter and linter (Prettier, html-validate, axe in CI), CMS or template engine, component library, which validator is authoritative — affects the shape of every example and the gates in `auditing.md`
- **Conventions** — attribute order and quoting, self-closing style on void elements, `data-*` naming, id and class conventions, comment density, one-file-per-component vs page templates — affects emitted markup
- **Platform** — target surfaces (web app, marketing site, email, print, embedded webview), device mix, whether RTL or CJK is in scope, minimum screen and pointer type — affects `media.md`, `email.md`, `internationalization.md`
- **Safety posture** — how loudly to flag accessibility regressions, whether third-party embeds and inline analytics may be emitted at all, appetite for `dangerously`-style raw HTML injection — affects Output Gates and `security.md`
- **Integrations** — analytics, consent management, icon and font providers, image CDN, the ESP an email will be sent through — affects `head.md`, `performance.md`, `email.md`
- **Constraints** — banned elements or embeds, a strict CSP regime, a no-JS requirement, legacy pages that must keep working, brand rules on titles and social images — affects what may be proposed at all
- **Output register** — diff vs whole file, how much of the rationale ships with the fix, whether alternatives are shown before the recommendation
- **Cadence** — accessibility re-audit, validation sweep, email client re-test, third-party embed review — every accepted cadence becomes a row in the `## Due` table of `memory.md`

## Traps

| Trap | Why it fails | Do instead |
|---|---|---|
| `<div onclick>` as a button | No keyboard, no focus, no role, no disabled state, no form submission | `<button type="button">`; style it flat (Rule 1) |
| `aria-label` on a `<div>` or `<span>` | Names are only exposed on elements with a role that supports naming — on a bare `div` it is ignored | Give it a role that takes a name, or use the right element |
| Fixing an ARIA warning by adding more ARIA | Redundant or conflicting roles override the native semantics that were already correct | Remove the role; `role="button"` on `<button>` is noise, `role="text"` erases children |
| Headings chosen by size | `h4` used for a section under an `h2` breaks the outline every screen-reader user navigates by | Level by depth, size by CSS (`semantics.md`) |
| `tabindex="1"` and above | Jumps that element ahead of the entire natural order, everywhere on the page | Only `0` and `-1` are ever correct (`accessibility.md`) |
| `<p>` used as a layout wrapper | Any block child closes it — you get two empty paragraphs and orphaned content | `<div>` for grouping, `<p>` for prose (Rule 9) |
| Nested `<a>` or `<button>` inside `<a>` | Illegal content model; the parser un-nests them and the click target becomes unpredictable | One interactive element per interactive element |
| `<br>` for spacing | Announced as line breaks and impossible to style responsively | Margin on block elements |
| Trusting `required` and `pattern` | Client validation is a UX affordance; anything can POST to the endpoint | Validate on the server; use the attributes for the keyboard and the error timing |
| `placeholder` used as the label | Vanishes on input, low contrast, and unreadable to anyone who tabs back | Visible `<label>` (Rule 4) |
| Lazy-loading the hero image | Defers the LCP element and defeats the preload scanner — measurably slower | `loading="eager"` and `fetchpriority="high"` on the LCP image (`performance.md`) |
| One `.ico` as the whole favicon set | Misses dark mode, high-DPI, and every mobile install surface | The three-file floor plus a manifest (`head.md`) |
| Blocklist sanitizing (`strip <script>`) | `<img onerror>`, `javascript:` URLs, SVG handlers, and mutation-XSS all survive it | Allowlist sanitizer, escaped by context (Rule 8) |
| `sandbox="allow-scripts allow-same-origin"` on same-origin content | The frame can reach into its own parent document and remove the sandbox attribute | Serve framed content from a distinct origin (`security.md`) |
| Copying the desktop page into an email | Flex, grid, `position` and external CSS are absent from the Word engine | Table layout, inline styles, 600px (`email.md`) |
| A markup decision that lives only in the chat | Re-litigated every quarter by whoever inherits the template | `artifacts/` with the date and what was rejected (`memory-template.md`) |

## Where Experts Disagree

- **How much ARIA is healthy.** One school treats every ARIA attribute as debt to be justified; the other builds design-system components on ARIA patterns because the native elements do not compose. The frontier is ownership: if a component has tests and a maintainer, an ARIA pattern is fine; if it is one page written once, the native element wins because nothing will maintain the contract.
- **`<section>` vs `<div>`.** `<section>` with no accessible name adds a region nobody can navigate to and clutters the landmark list; `<section aria-labelledby>` is genuinely useful. Default: `<div>` for grouping, `<section>` only when it has a heading you would want in the landmark list.
- **Web components in content sites.** Encapsulation and portability against a hard cost: nothing renders until the definition loads, and forms, labels and CSS all need explicit bridges across the shadow boundary. Declarative shadow DOM narrows the gap but does not close it (`templates.md`).
- **Semantic markup and SEO.** Search engines do not rank a `<article>` above a `<div>`, and claiming they do discredits the real argument: semantics buy assistive-tech behavior, browser features, and future parsers. Make the accessibility case; the metadata that genuinely affects search is in `head.md`.
- **Email builders vs hand-written email.** Builders survive client quirks that a hand-written template rediscovers; hand-written wins on size, dark mode control, and not being clipped at 102 KB. The frontier is whether anyone will re-test on client updates (`email.md`).

## Security & Privacy

**Credentials:** this skill reads and writes markup files. It does NOT store, log, copy, or transmit API keys, verification tokens, session values, or ESP credentials found in markup, and never writes one into `~/Clawic/data/`.

**Local storage:** preferences, page and template notes, rendering quirks, audit results and generated artifacts stay in `~/Clawic/data/html/` on this machine, plus hostname rows in the shared `~/Clawic/data/domains/`. URLs, element names and attribute names only — no secrets.

**Guardrails:** third-party embeds are named with the origin they contact and the capabilities they request before being proposed. Markup that renders user-supplied content is never emitted without its escaping or sanitizing step.

## Related Skills
More Clawic skills, get them at https://clawic.com/skills/html (install if the user confirms):
- `css` — layout, cascade, theming, and the visual half of everything here
- `javascript` — DOM scripting, events, and the behavior an ARIA pattern promises
- `accessibility` — audits, assistive-technology testing, and WCAG beyond the markup layer *(planned)*
- `seo` — search strategy, indexing, and content that ranks
- `svg` — inline vector graphics, `viewBox`, and icon systems

## Feedback

- If useful, star it: https://clawic.com/skills/html
- Latest version: https://clawic.com/skills/html

Part of [Clawic](https://clawic.com), the verified skill library. Get this skill: https://clawic.com/skills/html.
