diff --git a/public/.well-known/agent-skills/index.json b/public/.well-known/agent-skills/index.json index 8df87c1e..79feefc2 100644 --- a/public/.well-known/agent-skills/index.json +++ b/public/.well-known/agent-skills/index.json @@ -6,7 +6,7 @@ "type": "skill-md", "description": "Query and apply The Website Specification — a platform-agnostic specification of what a good website does. Use when the user asks what their site should have, whether something is required, how to audit a URL, what's missing for agent readiness, or anything else where you'd otherwise be guessing at web best practice. Backs answers with primary sources and ships an MCP server with search, list, fetch, checklist, and audit tools.", "url": "/.well-known/agent-skills/specification-website/SKILL.md", - "digest": "sha256:117eab2f07512f3520dd90acb13f733abf94e45489ce6feb75bc05a841360cc8" + "digest": "sha256:487b0ea2610fb2e4887e8dc7b682cd6de270934d6a9513409a3c202fd62b9a7c" } ] } diff --git a/public/.well-known/agent-skills/specification-website/SKILL.md b/public/.well-known/agent-skills/specification-website/SKILL.md index 615a3632..f675885d 100644 --- a/public/.well-known/agent-skills/specification-website/SKILL.md +++ b/public/.well-known/agent-skills/specification-website/SKILL.md @@ -5,7 +5,7 @@ description: Query and apply The Website Specification — a platform-agnostic s # specification.website -The Website Specification is a single source of truth for what a good website does. Ten categories, 163 pages, every item tagged with a status. It ships in three machine-readable forms: per-page Markdown, llms.txt / llms-full.txt, and an MCP server. +The Website Specification is a single source of truth for what a good website does. Ten categories, 164 pages, every item tagged with a status. It ships in three machine-readable forms: per-page Markdown, llms.txt / llms-full.txt, and an MCP server. ## When to use this skill diff --git a/public/og-default.png b/public/og-default.png index 5af51ae9..6ae92ff3 100644 Binary files a/public/og-default.png and b/public/og-default.png differ diff --git a/public/og/checklist.png b/public/og/checklist.png index fd07f4e9..f5d34d1a 100644 Binary files a/public/og/checklist.png and b/public/og/checklist.png differ diff --git a/public/og/spec.png b/public/og/spec.png index 6c7bee99..339214f2 100644 Binary files a/public/og/spec.png and b/public/og/spec.png differ diff --git a/public/og/spec/accessibility.png b/public/og/spec/accessibility.png index 45cb652e..57cf3d83 100644 Binary files a/public/og/spec/accessibility.png and b/public/og/spec/accessibility.png differ diff --git a/public/og/spec/accessibility/focus-not-obscured.png b/public/og/spec/accessibility/focus-not-obscured.png new file mode 100644 index 00000000..82a3228a Binary files /dev/null and b/public/og/spec/accessibility/focus-not-obscured.png differ diff --git a/src/content/changelog/2026-07-17-focus-not-obscured.md b/src/content/changelog/2026-07-17-focus-not-obscured.md new file mode 100644 index 00000000..62f7ffee --- /dev/null +++ b/src/content/changelog/2026-07-17-focus-not-obscured.md @@ -0,0 +1,8 @@ +--- +title: A focus ring behind a sticky header is still a failure +date: "2026-07-17" +type: added +relatedSlugs: [focus-not-obscured, focus-indicators, keyboard-navigation] +--- + +Added a page on [focus not obscured](/spec/accessibility/focus-not-obscured/), WCAG 2.2 Success Criteria 2.4.11 (AA) and 2.4.12 (AAA). Drawing a focus indicator and keeping it visible are separate problems with separate fixes: a perfect high-contrast ring fails the moment Tab scrolls a control under your sticky header, cookie banner, or chat widget, and the remedy is `scroll-padding` on the scroll container rather than anything in your focus styles. [Visible focus indicators](/spec/accessibility/focus-indicators/) now hands the topic off instead of covering it in a sentence. diff --git a/src/content/spec/accessibility/focus-indicators.md b/src/content/spec/accessibility/focus-indicators.md index ea2e4dcb..7ac17bd2 100644 --- a/src/content/spec/accessibility/focus-indicators.md +++ b/src/content/spec/accessibility/focus-indicators.md @@ -6,14 +6,14 @@ summary: "Whenever a control receives keyboard focus, the page must show a clear status: required order: 50 appliesTo: [all] -relatedSlugs: [keyboard-navigation, color-contrast, inert-attribute] -updated: "2026-05-29T09:13:20.000Z" +relatedSlugs: [keyboard-navigation, color-contrast, inert-attribute, focus-not-obscured] +updated: "2026-07-17T00:00:00.000Z" sources: - title: "WCAG 2.4.7 — Focus Visible (Level AA)" url: "https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html" publisher: "W3C" - - title: "WCAG 2.4.11 — Focus Not Obscured (Minimum) (Level AA)" - url: "https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html" + - title: "WCAG 1.4.11 — Non-text Contrast (Level AA)" + url: "https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html" publisher: "W3C" - title: "WCAG 2.4.13 — Focus Appearance (Level AAA)" url: "https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html" @@ -27,7 +27,7 @@ sources: A focus indicator is the visible cue that tells a keyboard user which control will receive their next keystroke. Browsers ship one by default — usually a thin outline ring. WCAG 2.4.7 (Level AA) requires the active focus location to be visible at all times when navigating by keyboard. -WCAG 2.2 added two more rules. 2.4.11 (AA) requires that the focused element is not entirely hidden behind sticky headers or cookie banners. 2.4.13 (AAA) sets minimum size and contrast for the focus indicator itself. +WCAG 2.2 added two more rules. 2.4.13 (AAA) sets a minimum size and contrast for the indicator itself. 2.4.11 (AA) requires that the focused element is not hidden behind sticky headers or cookie banners — a separate failure with a separate fix, covered in [focus not obscured](/spec/accessibility/focus-not-obscured/). This page is about drawing the indicator; that one is about whether anything is sitting on top of it. ## Why it matters @@ -63,7 +63,7 @@ Guidelines: - **At least 2 CSS pixels thick** to meet 2.4.13's enclosed-area minimum. - **Offset the outline** away from the control so it never disappears into the border. - **Two-tone rings** (a light ring and a dark ring) work on any background. -- **Keep focus visible across sticky UI.** Use `scroll-margin` so anchor jumps land below sticky headers, and avoid full-viewport modals that overlay the focused element below. +- **Keep focus visible across sticky UI.** A ring you drew correctly still fails if a sticky header covers it — see [focus not obscured](/spec/accessibility/focus-not-obscured/) for the scroll-padding fix. `:focus-visible` is supported in every current browser. If you need to support older engines, pair it with `:focus` as a fallback. @@ -72,11 +72,10 @@ Guidelines: - `outline: none` with no replacement. - A focus style that only changes the text colour by a small amount. - A focus ring drawn inside the button so the button's own background hides it. -- Sticky headers covering the focused control after Tab scrolls it into view. - Custom widgets (cards, tab lists) that never style their focused state. ## Verification - Tab through every page template; the focused control must be obvious from across the room. - Check the contrast of the focus ring against its background. -- Resize the viewport and test with sticky headers in place — focus must stay visible. +- Confirm nothing covers the ring once it is drawn — [focus not obscured](/spec/accessibility/focus-not-obscured/) has the sticky-header and cookie-banner checks. diff --git a/src/content/spec/accessibility/focus-not-obscured.md b/src/content/spec/accessibility/focus-not-obscured.md new file mode 100644 index 00000000..4658dc46 --- /dev/null +++ b/src/content/spec/accessibility/focus-not-obscured.md @@ -0,0 +1,95 @@ +--- +title: "Focus not obscured" +slug: focus-not-obscured +category: accessibility +summary: "A focused control must not be hidden behind a sticky header, cookie banner, or chat widget. WCAG 2.2 added this as a separate criterion because a perfect focus ring is worthless if something is sitting on top of it." +status: recommended +order: 55 +appliesTo: [all] +relatedSlugs: [focus-indicators, keyboard-navigation, skip-links, cookie-consent, inert-attribute] +updated: "2026-07-17T00:00:00.000Z" +sources: + - title: "WCAG 2.4.11 — Focus Not Obscured (Minimum) (Level AA)" + url: "https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html" + publisher: "W3C" + - title: "WCAG 2.4.12 — Focus Not Obscured (Enhanced) (Level AAA)" + url: "https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-enhanced.html" + publisher: "W3C" + - title: "WCAG 2.2 Recommendation" + url: "https://www.w3.org/TR/WCAG22/#focus-not-obscured-minimum" + publisher: "W3C" + - title: "MDN — scroll-padding" + url: "https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/scroll-padding" + publisher: "MDN" +--- + +## What it is + +**Focus Not Obscured** is a pair of WCAG 2.2 success criteria about what sits *on top of* the focused control. + +- **2.4.11 Focus Not Obscured (Minimum)**, Level **AA**: "When a user interface component receives keyboard focus, the component is not entirely hidden due to author-created content." +- **2.4.12 Focus Not Obscured (Enhanced)**, Level **AAA**: no part of the focused component may be hidden. + +The gap between them is the word **entirely**. At AA, a sliver of the control peeking out from under your sticky header is a pass. At AAA, any occlusion at all is a failure. AA is a floor, not a description of a good experience. + +"Author-created content" is the operative phrase. If the browser's own UI covers something, that is not your criterion to fail. If *your* sticky header, cookie banner, or chat bubble covers it, it is. + +### This is not the same as having a focus indicator + +The wrong belief to correct is that a well-styled focus ring discharges this. It does not, and the two criteria fail independently. + +[Visible focus indicators](/spec/accessibility/focus-indicators/) (2.4.7) asks whether you *drew* a focus indicator. Focus Not Obscured asks whether the user can *see* the one you drew. A site can have an immaculate three-pixel high-contrast ring and still fail 2.4.11 the moment Tab scrolls a link to the top of the viewport and a 64-pixel sticky header lands on it. The ring is rendering perfectly. It is simply underneath something. + +That is why WCAG 2.2 made it a separate criterion rather than a note on 2.4.7: the fix lives in your scroll and layout behaviour, not in your focus styles. + +## Why it matters + +A keyboard user cannot see where focus went. That is the whole problem, and it is indistinguishable from having no focus indicator at all — they press Tab, the page scrolls, and the thing they are now about to activate is behind a banner. They are one Enter keystroke away from a link they cannot read. + +Sighted keyboard users are the population this hurts most directly: people with motor disabilities who do not use a mouse, and people using switch access or voice control that drives the focus ring. Screen reader users are largely unaffected, because the content is announced regardless of what covers it — which is precisely why this bug survives so long. It is invisible to the automated checks and to the screen-reader pass, and it only shows up when a sighted person tabs through the page and watches. + +The failure is also intermittent, which makes it easy to dismiss. Focus is obscured only when the focused element happens to land in the strip the sticky element occupies. Tab through the top of the page and everything is fine. Tab into the middle and a control vanishes. + +## How to implement + +Most failures come from one of three sources: sticky headers and footers, non-modal cookie banners, and floating chat or feedback widgets. + +**Reserve the sticky area in the scroll container.** When focus moves off-screen, the browser scrolls the element into view — and by default it stops as soon as the element touches the viewport edge, which is exactly where your sticky header lives. `scroll-padding-top` on the scrolling element tells the browser to treat that strip as unusable: + +```css +:root { + /* Match the sticky header's height. */ + scroll-padding-top: 5rem; +} +``` + +Add `scroll-padding-bottom` if you also have a sticky footer. If the header's height changes responsively, drive both from the same custom property so they cannot drift apart. + +**Prefer `scroll-padding` over `scroll-margin` for this.** `scroll-margin` is set per-target and you will forget one; `scroll-padding` is set once on the scroll container and applies to everything scrolled into it — including focus. + +**Make banners modal or make them get out of the way.** A cookie banner that is non-modal and sticky is the canonical failure: focus can travel to the content behind it and end up underneath it. Either make it a true modal — trap focus inside it, and mark the rest [`inert`](/spec/accessibility/inert-attribute/) so focus cannot reach the obscured content in the first place — or ensure it does not overlap focusable content. See [cookie consent](/spec/privacy/cookie-consent/). + +**Two exceptions are written into the criteria**, and both are narrower than people assume: + +- **User-movable content.** Only the *initial* position is evaluated. If the user drags your panel over a control themselves, that is not your failure. If it starts there, it is. +- **User-opened content.** Content the user opened may obscure the focused component, but only if they can reveal it again *without moving focus* — pressing Escape, or scrolling. A disclosure the user cannot dismiss without tabbing onward does not qualify. + +Neither exception covers "the user could have dismissed our banner". It was your content, placed there by you, in its initial position. + +## Common mistakes + +- A sticky header with no `scroll-padding-top` on the scroll container — by far the most common cause. +- `scroll-margin-top` applied to headings for anchor links, which fixes in-page jumps but does nothing for Tab. +- A non-modal cookie banner pinned to the bottom of the viewport, with the page behind it still focusable. +- A chat widget in the bottom-right corner that covers the last control in a form. +- Treating the AA "entirely hidden" threshold as the goal. Two visible pixels is a pass and still unusable. +- Assuming a screen-reader pass covers it. It does not — the content is announced from under the banner just fine. +- Testing only at desktop width. Sticky elements grow proportionally larger as the viewport shrinks. + +## Verification + +- Tab through every template from the top of the page to the bottom and *watch the focus ring the whole way*. Any control that disappears under a sticky element is a failure. This is a purely visual check; no tooling will do it for you. +- Repeat it at a narrow viewport and at 200% zoom, where sticky elements eat proportionally more of the screen. +- Repeat it with the cookie banner showing — that is a fresh visitor's first experience, and the state you are least likely to test. +- Scroll to the middle of a long page, then Tab. Failures cluster where the page is already scrolled. +- For AAA (2.4.12), check that no part of the control is covered, not merely that some of it shows. diff --git a/src/content/spec/accessibility/inert-attribute.md b/src/content/spec/accessibility/inert-attribute.md index beb027b2..d20cf602 100644 --- a/src/content/spec/accessibility/inert-attribute.md +++ b/src/content/spec/accessibility/inert-attribute.md @@ -6,7 +6,14 @@ summary: "When an overlay is open, the content behind it should be unreachable status: recommended order: 65 appliesTo: [all] -relatedSlugs: [keyboard-navigation, focus-indicators, skip-links, native-interactive-elements] +relatedSlugs: + [ + keyboard-navigation, + focus-indicators, + focus-not-obscured, + skip-links, + native-interactive-elements, + ] updated: "2026-06-26T09:00:00.000Z" sources: - title: "HTML Standard — The inert attribute" diff --git a/src/content/spec/accessibility/keyboard-navigation.md b/src/content/spec/accessibility/keyboard-navigation.md index 45bbe25b..9bfadcb7 100644 --- a/src/content/spec/accessibility/keyboard-navigation.md +++ b/src/content/spec/accessibility/keyboard-navigation.md @@ -6,7 +6,15 @@ summary: "Every interactive element on the page must be reachable and operable w status: required order: 40 appliesTo: [all] -relatedSlugs: [focus-indicators, skip-links, semantic-html, inert-attribute, dragging-movements] +relatedSlugs: + [ + focus-indicators, + focus-not-obscured, + skip-links, + semantic-html, + inert-attribute, + dragging-movements, + ] updated: "2026-05-29T09:13:20.000Z" sources: - title: "WCAG 2.1.1 — Keyboard (Level A)" diff --git a/src/content/spec/privacy/cookie-consent.md b/src/content/spec/privacy/cookie-consent.md index 09b88338..833664df 100644 --- a/src/content/spec/privacy/cookie-consent.md +++ b/src/content/spec/privacy/cookie-consent.md @@ -6,7 +6,14 @@ summary: "In the EU and UK, non-essential cookies and similar storage require fr status: required order: 20 appliesTo: [all] -relatedSlugs: [privacy-policy, third-party-scripts, analytics-privacy, global-privacy-control] +relatedSlugs: + [ + privacy-policy, + third-party-scripts, + analytics-privacy, + global-privacy-control, + focus-not-obscured, + ] updated: "2026-07-09T00:00:00.000Z" sources: - title: "EDPB Guidelines 03/2022 on deceptive design patterns in social media"