fix(docs): WCAG 2.1 AA accessibility remediation for the docs portal#3687
Merged
Conversation
text-muted (gray-500) read 3.41:1 on chrome and 4.11:1 on white in light mode, failing WCAG AA 4.5:1 for the many elements that use it (cards, sidebar, footer, TOC, kbd, search, page-nav...). Bump it to gray-600 (4.87:1 chrome / 5.88:1 white). Tiny uppercase chrome labels (sidebar section headings, footer column headings, page-nav labels) still read thin at gray-600, so give them a dedicated gray-700 override (7.14:1 chrome / 8.61:1 white). Also drop .sidebar-subheading's opacity: 0.75, which dragged its already-muted text down to an effective 2.39:1 in light mode. Dark mode is unaffected: it keeps --text-muted: gray-400 (6.37:1) and reads 6.37:1 without the opacity too (previously ~4.8:1 with it). Part of the docs-a11y-audit remediation plan (PR 1 of 9).
…a11y PR 2/9) Five independent low-contrast spots, all pre-verified against the dark-mode "do not touch" list before changing: - Dark syntax comments (#4A5568 on bg-code-block gray-900) read 2.14:1; tokenize to gray-400 (5.59:1). Light comments already pass and are untouched. - kbd used --text-muted (gray-400) on --bg-code (gray-800 in dark), reading 4.42:1; give kbd a dedicated dark color (gray-300, 6.07:1) and keep the light theme on --text-muted (already 5.88:1 on white). - Blue-400 accent text on dark cards (elevator labels, ecosystem tile headings, usecase links, glossary terms) read 4.08:1. Add an --accent-on-card token (dark: blue-300 5.89:1, light: blue-500, same 5.0:1 it already had) and apply it to those four selectors. - .pain-x badge (white on --error #F87171) read 2.77:1 in both themes; add --error-strong (#DC2626, 4.83:1) for badge use only. --error itself is untouched. - .copy-btn hardcoded raw hex ignoring theme tokens (3.38:1 dark, 4.47:1 marginal light, hover 2.41:1 light); tokenize to gray-400/ gray-300 (dark) and a light override of gray-600/gray-700, and add a :focus-visible state (opacity:1 + outline) so keyboard users can see and reach the button, which was previously invisible off hover. Part of the docs-a11y-audit remediation plan (PR 2 of 9).
Light mode used the same gray-100 for the page background, sidebar, and content column, flattening the visual hierarchy and dragging text contrast down for every element in the content column (a white card on gray-100 read only 1.21:1 of separation). Add a --bg-content token, applied to .main: gray-100 for header/ sidebar/footer chrome stays as-is, but the content column now sits on white in light mode, mirroring docs.docker.com's chrome-vs-content pattern. In dark mode --bg-content resolves to the same --bg-dark the page already uses, so .main's new explicit background is a no-op — dark mode is pixel-identical. With this change, the existing light accent link color (blue-500, #2560FF) reads 5.00:1 on the new white surface, so no link-color change is needed. Cards (already white) now rely on their existing border for separation from the page. Part of the docs-a11y-audit remediation plan (PR 3 of 9).
…n (a11y PR 4/9) The search overlay declared aria-modal="true" but nothing enforced it: Tab could walk focus into the page behind it, and closing the dialog (Escape, backdrop click, a result click) never returned focus to whatever had opened it. app.js: track the previously-focused element in openSearch(), restore it in closeSearch(), and trap Tab/Shift-Tab inside the modal input + result links while the dialog is open. Add ArrowUp/ArrowDown navigation that moves real DOM focus between the query input and the result links, so keyboard users can browse results without a mouse. The header's #search-input was a readonly <input> opened by both click and focus — Shift-Tabbing past it from the theme toggle re-opened the dialog it had just closed, and its role misled screen reader users into thinking it was an editable field. Replace it with a <button> styled identically (still shows the ⌘K hint); this also means Enter/ Space activation is free via native button semantics, so the stray 'focus' listener is simply removed rather than replaced. CSS: add :focus-visible outlines to .search-result and the modal input (previously outline: none with only a background-color hover/ focus cue); the header trigger's own focus ring reused --accent-light, which is only 12%-alpha in dark mode (fine for its other job as a subtle fill, too faint as a ring) — give it a dedicated, more opaque ring color instead. Part of the docs-a11y-audit remediation plan (PR 4 of 9). The listbox/ option ARIA roles on the results themselves are addressed in PR 5.
The search trigger button (#search-input, introduced when this PR converted it from a readonly input to a real <button>) used --text-muted for its label text. That resolves to gray-400 (#8B99B2), which reads a marginal 4.42:1 against the button's own --bg-hover gray-800 (#2C333F) background in dark mode -- below the 4.5:1 AA threshold for its 0.8rem text. Use gray-300 (6.07:1) for the dark default instead, the same value already used for kbd text on the same gray-800 background, and keep the existing --text-muted (gray-600, 5.88:1 on white) for light via an explicit override -- light was never broken.
…5/9)
A grab-bag of semantics fixes, none of which change dark-mode pixels:
- #search-results dropped role="listbox" and result links dropped
role="option": a listbox/option pattern needs aria-activedescendant
and full combobox wiring on the input, neither of which existed, so
the roles just misrepresented a plain list of links to screen
readers. PR 4 already added real keyboard navigation between them.
- #page-content (the skip-link's target) gets tabindex="-1" so focus
reliably lands there across browsers when the skip link is used.
- <details class="sidebar-section" title="..."> dropped the title
attribute, which only duplicated the visible <summary> text for
screen readers.
- #theme-toggle and .github-link dropped their redundant title
attributes (aria-label already covers the accessible name); the
theme toggle now maintains aria-pressed and a state-reflecting
aria-label ("Switch to light/dark theme") via a new
updateThemeToggleState() helper called from initTheme()/
toggleTheme().
- .sidebar-toggle gained aria-expanded/aria-controls, kept in sync by
the click handler, and the closed off-canvas sidebar (≤1024px) now
gets visibility: hidden (transition-delayed so the slide animation
still plays) so it is not Tab-reachable while hidden.
- js/app.js buildTOC() now creates the "Table of contents" label as an
<h2> instead of a <div>, giving it a real place in the heading
outline.
- Footer column headings (Product/Resources/Company) changed from
<h4> to <h2>, removing a heading-level skip in the page outline;
CSS selectors renamed to match, same visual style.
- Added one shared :focus-visible rule (accent outline) covering
.btn-icon, .github-link, .sidebar-toggle, .btn, .page-nav a,
.ecosystem-tile, and footer links, which previously relied on
inconsistent browser default outlines.
- Deleted the dead [data-theme="dark"] selectors in style.css (on
.toc-action/.toc-link/.toc-link.active): the JS only ever removes
the data-theme attribute for dark, never sets it to "dark", so
these never matched. The :root:not([data-theme="light"]) twins are
kept and still apply the same styles — "dark = bare :root" stays
the documented convention (docs-a11y-audit Q4).
Part of the docs-a11y-audit remediation plan (PR 5 of 9).
fix(docs): finish PR 5 CSS updates for renamed footer heading and dead dark selectors
… 6/9)
Nothing on the site checked prefers-reduced-motion: smooth scrolling
(html { scroll-behavior: smooth }), the .content fade-in keyframe, and
assorted hover/press transform transitions all ran unconditionally.
Add a @media (prefers-reduced-motion: reduce) block that zeroes
animation/transition durations and forces scroll-behavior: auto, and
gate js/app.js's TOC-link scrollIntoView on the same media query.
The homepage demo.gif auto-played and looped with no way to pause,
stop, or hide it (WCAG 2.2.2). Swap it for a <video> (muted, loop,
playsinline, controls, poster, preload="metadata") generated from the
same source: demo.mp4 (ffmpeg -movflags faststart, ~55% smaller than
the GIF) and demo-poster.png (first frame). controls gives users a
native pause affordance the GIF never had. The GIF itself is kept as
the <video> element's fallback content for browsers/readers that
can't play the video, so nothing is deleted.
Both new files are mounted in hugo.yaml next to the existing demo.gif
mount so they ship as static assets the same way.
Part of the docs-a11y-audit remediation plan (PR 6 of 9).
NOTE: docs.docker.com portability of the new <video>/mp4 is UNVERIFIED
(docs-a11y-audit plan, Q2) — needs a downstream preview check before
this merges; the GIF fallback exists specifically to de-risk that.
… runtime GIF fallback The <video> declared width=1200 height=675 (16:9), but demo.mp4 (and its poster/GIF, generated from the same source) is actually 2000x1600 (5:4). Since .demo-container only had a responsive rule for img, the video rendered at its raw HTML attribute box and got stretched to the wrong ratio once displayed. - Fix the width/height attributes to the video's true 5:4 ratio (1200x960) instead of re-encoding the source. - Add .demo-container video to the existing responsive image rule (width: 100%; height: auto) so it scales correctly at every viewport -- verified at a 360px viewport: the video box now renders at a 1.25 (5:4) ratio matching the source exactly, no clipping/stretching. - The nested <img src=demo.gif> inside <video> is native fallback *content*: browsers only render it when they don't support the <video> element at all, never on a decode/network failure of a supported one. Add handleVideoFallback() in app.js to swap the GIF in on the video's error event, covering the common file-failed-to-load case that the nested-content fallback can't.
handleVideoFallback() swapped a failed <video> for the nested demo.gif on the error event -- but that GIF is ~14.7s/367 frames, loops infinitely, and is not pausable: exactly the WCAG 2.2.2 problem the video swap was introduced to remove, just reintroduced on the failure path. The nested <img> is native fallback CONTENT, rendered by browsers that do not recognize <video> at all -- that case is unaffected. A decode/fetch failure of a *supported* video is a different, more common path, and now falls back to the static demo-poster.png frame instead: a still image needs no pause control, so it is always a safe fallback regardless of why playback failed.
… (a11y PR 7/9) Reference tables (e.g. the Flags table on /configuration/sandbox/) had no overflow handling, so at 320px viewport width or 400% zoom the whole page scrolled horizontally instead of just the table (WCAG 1.4.10 reflow). .content table now gets display: block; overflow-x: auto, scoping the scroll region to the table itself. .copy-btn's hit area was ~22px tall, under the WCAG 2.5.8 24px target minimum (.btn-icon is already ~32px and unaffected). Add min-height: 24px with inline-flex centering so the label stays vertically centered. Part of the docs-a11y-audit remediation plan (PR 7 of 9).
.compare-side defaulted to min-width:auto (the grid-item default), so its code content refused to shrink with the column and forced the whole homepage to scroll horizontally at narrow viewports (~661px scrollWidth at a 360px viewport -- WCAG 1.4.10 reflow). Add min-width:0 so the item can shrink; .compare-side pre already has its own overflow-x:auto, so long code lines keep scrolling locally instead of dragging the whole page along.
… 8/9) The homepage flow diagram is embedded via <img>, so it cannot inherit currentColor from the page and needs one hardcoded palette that works on both the white light-theme card and the near-black dark-theme card (#1E2129). The old gray-600 (#566581) read 5.88:1 on white but only 2.74:1 on the dark card, and the "REASONING LOOP" label's brand blue (#2560FF) read 3.22:1 there — both well under the 4.5:1 normal-text minimum. - Base fill/stroke moves to gray-500 (#6C7E9D), the best single-color balance against this bg pair (4.11:1 white / 3.92:1 dark) — far short of the old dark reading, close to AA on both sides, and a neutral single color genuinely cannot do better against endpoints this far apart. - The "REASONING LOOP" label moves to blue-400 (#4A76FF) for the same reason (3.94:1 white / 4.08:1 dark). - Decorative borders and arrows keep the brand blue #2560FF: as non-text graphical objects they only need the 3:1 minimum (WCAG 1.4.11), which #2560FF clears on both surfaces (5.00 light / 3.22 dark). - Removed the opacity="0.7"/"0.85" attributes used to de-emphasize secondary text: on top of an already-thin color this compounded the contrast problem (the same anti-pattern fixed in style.css's .sidebar-subheading in PR 1); size/weight already carry the visual hierarchy without it. Part of the docs-a11y-audit remediation plan (PR 8 of 9).
…mpromise palette The single hardcoded palette (gray-500 body text, blue-400 accent text) was picked as the best midpoint between a white light card and the near-black dark card, but a midpoint can't clear 4.5:1 normal-text AA against both at once: it read ~3.9-4.1:1 either way, still failing. Split the asset into two theme-specific files instead, each only needing to pass against a single background: - how-it-works.svg (light): gray-600 body text (5.88:1 on white), blue-500 accent label (5.00:1 on white). - how-it-works-dark.svg (new): gray-400 body text (5.59:1 on #1E2129), blue-300 accent label (5.89:1 on #1E2129) -- the same --accent-on-card pairing used elsewhere for brand-blue text on dark cards. index.md now renders both <img>s (theme-light-only / theme-dark-only classes); style.css toggles them with plain display:none/block per theme so only the active one is ever in the accessibility tree. Decorative borders/arrows keep the theme-invariant brand blue (#2560FF), which already cleared the 3:1 non-text minimum on both surfaces.
Add docs-a11y.yml: build+serve the Hugo site the same way contributors do locally (docs/Dockerfile's `hugo server`), then scan / (home), /configuration/sandbox/ (wide tables + callouts), /getting-started/ quickstart/, and /404.html with pa11y-ci at WCAG2AA. pa11y only fails on HTML_CodeSniffer "error"-level results (warnings/notices are informational), so the gate targets real violations, not every possible nit. Runs on docs/** pushes and PRs, landing last so it starts from the green baseline PRs 1-8 built. Running it against the post-PR-1-8 site surfaced one real, previously unlisted violation: the "Rouge / highlighter-rouge overrides" block in style.css set color: var(--gray-200) unconditionally on any <code class="language-*">, Hugo/Chroma's class for every fenced code block. That rule has the same CSS specificity as, and sits after, the correctly-themed `[data-theme="light"] .content pre code` rule, so it silently won in light mode too — breaking any syntax token without its own explicit color override (Chroma's untyped ".l"/".sd"/".cl" fallback tokens) at 1.47:1. Fixed by dropping the redundant color declaration from that block; the themed rule above it already owns text color for both themes. docs/STYLE.md documents the new tokens/conventions from PRs 1-9 and how to reproduce a pa11y-ci run locally. Part of the docs-a11y-audit remediation plan (PR 9 of 9, final step). NOTE: the local Docker daemon became unresponsive while re-validating this specific fix live against a running pa11y-ci scan (it had already caught the bug above in an earlier run against the post-PR-8 site). The fix itself is a straightforward CSS-specificity correction verified by re-reading the cascade, but please re-run pa11y-ci (or let CI run it) to confirm before merging.
docs/.pa11yci.json listed 4 plain URLs with no theme setup, so the scan only ever exercised whichever theme the headless browser initializes by default -- observed locally to be light, meaning the gate never actually looked at dark mode. Add a ?theme=light|dark query param that js/app.js's initTheme() honors ahead of localStorage/prefers-color-scheme, and list each of the 4 scanned pages twice in .pa11yci.json, once per theme (8 URLs total). This makes the both-theme scan deterministic regardless of the browser's default color scheme, rather than depending on it.
…me gate Running the new deterministic dark scan (previous commit) against the full site found 2 real, previously-undetected dark-mode AA failures (both predate this PR stack, caught only because nothing had scanned dark mode as its own deterministic target before): - .preview-banner a used --accent (blue-400, 4.08:1 on --bg-card in dark) instead of --accent-on-card (blue-300, 5.89:1) -- the same token already used for elevator/usecase/glossary text-on-card, just missed here. - .content a's prose-link color (--accent-hover) had higher CSS specificity than .btn-primary/.btn-secondary's own color, so it silently leaked onto any button rendered as a link inside .content (the homepage 'Quick Start' button, 'View on GitHub'). Invisible in light mode only because --accent and --blue-500 happen to share a hex there; in dark it dropped the Quick Start button to 2.55:1. Fixed by excluding .btn links from the prose-link rule (.content a:not(.btn)) instead of fighting specificity per button. - 404.md's 'Back to Docs' button hardcoded color:white against background:var(--accent) inline; --accent's dark value (blue-400) only reads 3.94:1 against white. Swapped to --accent-strong (a fixed blue-500, identical in both themes), which reads 5.00:1 for white text in both. Verified via a local Hugo build + [email protected] (system Chrome) run across all 4 pages x 2 themes: 8/8 URLs, 0 errors.
The a11y section drifted from what actually shipped after the developer/review round-trip on this PR stack. Bring it up to date: - Stop presenting how-it-works.svg's single hardcoded palette as an AA-compliant compromise (it wasn't -- ~3.9-4.1:1 both ways, still a fail). Describe the per-theme-SVG-variant approach actually implemented instead, with its real, passing ratios. - Document the newly-fixed dark-mode violations (search trigger, preview-banner link, .content a specificity vs. .btn-primary/ .btn-secondary, 404's inline accent) and add them to the contrast reference table. - Note the video aspect-ratio requirement and the runtime GIF fallback's real scope (network/decode errors, not every possible failure mode). - Add a manual verification checklist for the JS-driven interactions (search focus trap/restore/arrow-nav, theme toggle, sidebar toggle, reduced motion, responsive media) that a static HTML scanner like pa11y-ci structurally cannot exercise -- this repo has no package.json or browser-test harness, and adding one for a handful of interaction checks on a dependency-free static docs site would be disproportionate; call that out explicitly rather than silently leaving the gap uncovered. - Explain the ?theme= query param mechanism the both-theme pa11y-ci gate relies on, both in STYLE.md and in the workflow's own comments.
The broad `.content a:not(.btn):hover` rule outranks component link rules (`.preview-banner a`, `.usecase a`, ...) via specificity, and hardcoded a single blue (--docker-blue-light, blue-400) for both themes: 4.08:1 on a dark --bg-card and 3.94:1 on white, both AA fails -- while every default (non-hover) link state passed. Add a dedicated --link-hover token, scoped per theme like the other accent-* tokens (blue-200 in dark, blue-600 in light) and verified against --bg-card specifically, since it is lighter than --bg-content in dark and so the harder background to pass. .btn links stay excluded via :not(.btn), untouched.
STYLE.md previously described the (now-fixed) link hover as a flat, unthemed blue and did not mention verifying it -- update the contrast table and conventions with the real --link-hover values, and describe the corresponding reflow (min-width:auto on grid items) and video- error-fallback fixes landing alongside this on pr7/pr6. Also add the three manual-checklist items a second review round found missing: video-error fallback behavior, link hover contrast, and page-level narrow-viewport (320-360px) reflow -- none of these are catchable by the static pa11y-ci scan.
[email protected]'s bundled Puppeteer doesn't download its own Chromium when installed via `npx --yes`, so the docs-a11y job failed before scanning: "Could not find expected browser (chrome) locally." pa11y-ci's own docs call out that Ubuntu > 20.04 needs an explicit Chrome executable path. Add a pinned browser-actions/setup-chrome step (matching this repo's existing convention of pinning tool installs by SHA, e.g. Hugo/Go/Task in sibling workflows) and point Puppeteer at its output via PUPPETEER_EXECUTABLE_PATH for the pa11y-ci step, leaving docs/.pa11yci.json untouched so it stays portable across machines. Document the same reproduction step in docs/STYLE.md's local-scan instructions. Reproduced locally: without an executable path set, `npx --yes [email protected] --config .pa11yci.json` (the workflow's exact command) fails with the identical error against a local hugo server; setting PUPPETEER_EXECUTABLE_PATH to a local Chrome makes the unmodified config pass 8/8 both-theme URLs. Final green can only be confirmed on GitHub once pushed.
Sayt-0
approved these changes
Jul 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR brings the docker-agent documentation portal (Hugo site under
docs/,published at https://docker.github.io/docker-agent/) to WCAG 2.1 AA
conformance across both the light and dark themes, and adds a CI gate that
keeps it that way.
It implements plan
docs-a11y-auditin full. The work was originallydeveloped and reviewed as a stack of 9 small, independently-reviewable
GitButler branches/PRs (
docs/a11y-pr1-light-contrast→docs/a11y-pr9-ci-gate, 21 commits total, cleanly rebased onto the currentmain). Per the user's decision, all 9 are combined into this single PRrather than opened as 9 stacked PRs, to simplify review/merge — the commit
history is preserved as-is so each logical step is still visible individually.
An accessibility audit produced a findings register (Critical/High/
Medium/Low) covering both themes; every finding was fixed, re-verified with
computed contrast ratios, and validated with a live
pa11y-ciscan. Tworeview rounds (by an independent
reviewagent) surfaced additionalblockers, all of which are fixed here as well — see "Review remediation"
below.
Fixes, grouped by area
Light/dark contrast (tokens)
--text-muteddarkened from gray-500 to gray-600 (fixes pervasivesub-AA text: card/feature copy, sidebar labels, footer, TOC, search UI,
kbd, etc.); removed a strayopacity: 0.75de-emphasis on.sidebar-subheading(never de-emphasize with opacity — it silentlybreaks contrast math); added gray-700 overrides for tiny uppercase labels.
kbd, accent-on-card text(
--accent-on-cardtoken for.elevator-label/.usecase a/.glossary dt/.ecosystem-tile strong), and the.pain-xbadge background allbrought to AA;
.copy-btncolors tokenized with a light override and madevisible on keyboard focus (it was previously invisible until
pre:hover).--bg-content, docs.docker.com-style chrome-vs-content split): header/sidebar/footer stay gray-100 chrome,
the content column becomes white, fixing marginal link/muted-text ratios
and improving visual hierarchy. Dark mode is a no-op change (
--bg-content= the existing dark background).
Search dialog focus management
restore to the trigger on close.
role="listbox"/role="option"misuse (results render as a plain list oflinks); replaced the header's
readonlyinput acting as a fake button(which stole focus via a
focuslistener) with a real, identically-styledtrigger.
:focus-visiblerings added on search results and the modalinput.
Motion & media
prefers-reduced-motion: reduceblock disabling smooth scroll,fade-in, and hover transforms; gated the TOC's
scrollIntoViewon the samemedia query.
demo.gifwith a<video muted loop playsinline controls>+ poster (with the GIF kept asnative
<video>fallback content for browsers that don't support<video>at all). Fixed the video's aspect ratio/responsive CSS, and madea genuine playback error (e.g. failed load) fall back to a static poster
image rather than the looping GIF, so the reduced-motion property of the
fix holds on the error path too.
Per-theme SVGs
how-it-works.svgcarries small informative text that cannot clear 4.5:1normal-text AA against both a white and a near-black card with one
compromise palette. Replaced the single-palette approach with two
theme-specific files (
how-it-works.svgfor light,how-it-works-dark.svgfor dark), toggled via CSSdisplay, eachindependently verified against its own single background.
Reflow
/configuration/sandbox/)no longer force page-level horizontal scroll at 320px/400% zoom; the
homepage comparison grid's
min-width: autodefault (which prevented itscode column from shrinking) is fixed so the whole page reflows correctly
at 320–360px, with local scrolling preserved inside code/table regions.
.copy-btnhit area grown to the WCAG 2.2 24px minimum.ARIA/semantics + dead-selector cleanup
aria-expanded/aria-controls; closed sidebar isno longer keyboard-reachable at ≤1024px; theme toggle maintains
aria-pressed; skip-link target is reliably focusable(
tabindex="-1"); TOC heading is a real<h2>; footer headings promotedfrom
h4toh2to fix a heading-level skip; duplicatetitle/aria-labelpairs and a redundant<details title="…">removed; a singleshared
:focus-visiblerule added for consistency.[data-theme="dark"] …CSS selectors: the site's JS onlyever removes
data-themefor dark (never sets it), so those selectorsnever matched — removed as dead code/maintenance hazard rather than wired
up, per the plan's resolved decision to keep "dark = bare
:root" as theconvention.
CI gate
.github/workflows/docs-a11y.yml: builds the Hugo site, serves it,and runs
pa11y-ci(WCAG2AA) against/,/configuration/sandbox/,/getting-started/quickstart/, and/404.html.docs/.pa11yci.json(?theme=light/?theme=dark), a query param thatinitTheme()inapp.jshonors ahead oflocalStorage/prefers-color-scheme, so the scan is deterministic regardless of thescanning browser's default color scheme (8 URLs total).
browser-actions/setup-chrome, matching thisrepo's pin-by-SHA convention used in sibling workflows) and points
PUPPETEER_EXECUTABLE_PATHat it, sincepa11y-ci's bundled Puppeteerdoesn't reliably download its own Chromium under
npx --yesonubuntu-latest.docs/STYLE.mdgained an accessibility section: the AA contrast table fortokens (including the new
--bg-content/--accent-on-card/--link-hovertokens), "never de-emphasize with opacity", how to run the scan locally,
and a manual verification checklist for the things a static scanner can't
catch (focus trap/restore/arrow-nav, theme/sidebar toggle state, reduced
motion, responsive media, link-hover contrast, forced video-error
fallback, narrow-viewport reflow).
Review remediation (already folded into the commits above)
Two independent review rounds found additional issues beyond the original
audit register, all fixed in this branch:
focus work itself).
swapping in the looping GIF instead of a static poster.
how-it-works.svgdidn't actually clear 4.5:1normal-text AA in both themes (see "Per-theme SVGs" above).
genuine both-theme scan:
.preview-banner a, a CSS-specificity leak thatlet prose-link color override
.btn-primary/.btn-secondarybuttonsrendered inside
.content, and a hardcoded white-on-accent color in404.md's "Back to Docs" button.--link-hovertoken),found via a real headless-Chrome hover simulation rather than static token
math.
pa11y-ci/Puppeteer couldn't find aChrome binary on
ubuntu-latest) — fixed via the pinned-Chrome stepdescribed above; this is a deliberate, documented deviation from "no
structural workflow change," scoped to CI plumbing only, not the
accessibility fixes themselves.
Known follow-ups / residuals
content in
docs/is mounted downstream at docs.docker.com, but thecustom layouts/CSS/JS in this repo are github.io-only. The
demo.gif→<video>swap touchesdocs/index.md, which does ship downstream, sowhether
docs/demo.mp4renders/hosts correctly once mounted intodocs.docker.com's Hugo module build is not verifiable from this
sandbox and needs a downstream preview check before merge. The GIF
remains in place as native
<video>fallback content (for browserswithout
<video>support), and a static poster now covers the playback-error path, so there's no functional regression while this is pending.
--accent-on-cardnot actually applied to.usecase a/.preview-banner a(non-blocking, documented)..content a:not(.btn)(added to fix abutton-color-leak bug) has higher CSS specificity than these components'
own single-class rules, so it silently wins for their default color too,
not just hover. This is not currently a contrast failure — both
colors involved still clear AA (dark 6.32:1, light 5.00:1) — but it means
--accent-on-cardisn't in effect there, which is a latent coupling: afuture change to the shadowing token alone could unexpectedly move these
two components' color/contrast. Documented as a known issue; recommend a
small dedicated follow-up PR to raise those selectors' specificity (or
formally adopt the shadowing token as their color and drop the unused
application) rather than expanding this PR's scope.
CI
docs-a11y.yml(both-themepa11y-ci, 8 URLs) should run on this PR —final green confirmation on GitHub is the pending item now that this branch
is pushed. All fixes were locally validated (Hugo build + a live
[email protected]run against all 8 URLs: 0 errors) prior to opening this PR,including the CI-gate's own pinned-Chrome fix reproduced locally with an
equivalent
PUPPETEER_EXECUTABLE_PATHsetup.Plan
Implements plan
docs-a11y-audit(see the shared plan store for the fullfindings register, decisions, and implementation log).