fix(website): structural accessibility (landmarks, icon-link names, dropdown state)#5469
fix(website): structural accessibility (landmarks, icon-link names, dropdown state)#5469mmabrouk wants to merge 1 commit into
Conversation
…dropdown state Zero visual change (pixel-verified). Color/contrast deliberately untouched per founder decision. Claude-Session: https://claude.ai/code/session_013Qe1Vf2yvj33BVd5W1q7HB
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Website previewPreview URL: https://pr-5469-agenta-website-preview.mahmoud-637.workers.dev Built from |
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe website adds semantic main landmarks, synchronizes navigation dropdown ChangesWebsite accessibility refinements
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dc92a71c-800d-4fcc-bd89-b34c113cfa87
📒 Files selected for processing (13)
website/src/components/OpenStandards.astrowebsite/src/components/SiteNav.astrowebsite/src/components/TemplateExplorer.tsxwebsite/src/content/posts/introducing-custom-workflows.mdxwebsite/src/content/posts/introducing-prompt-registry.mdxwebsite/src/content/posts/july-2025-product-updates.mdxwebsite/src/content/posts/launch-week-2-day-1.mdxwebsite/src/content/posts/launch-week-2-day-2-online-evaluation.mdxwebsite/src/content/posts/launch-week-2-day-3-evaluation-sdk.mdxwebsite/src/content/posts/prompt-playground.mdxwebsite/src/content/posts/structured-outputs-playground.mdxwebsite/src/layouts/Site.astrowebsite/src/pages/index.astro
| dd.addEventListener("mouseenter", () => sync(true)); | ||
| dd.addEventListener("mouseleave", () => sync(false)); | ||
| dd.addEventListener("focusin", () => sync(true)); | ||
| dd.addEventListener("focusout", () => { | ||
| // focusout fires before focus lands on the next target; defer so we can | ||
| // read whether focus is still inside this dropdown. | ||
| requestAnimationFrame(() => sync(dd.contains(document.activeElement))); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Derive aria-expanded from both hover and focus state.
mouseleave can set aria-expanded="false" while focus remains inside the dropdown, and deferred focusout can do the same while the pointer still hovers. Since the CSS keeps the panel open when either :hover or :focus-within matches, the ARIA state can contradict the visible state. Make sync evaluate both selectors instead of accepting a single boolean.
Proposed fix
- const sync = (open: boolean) =>
- trigger.setAttribute("aria-expanded", open ? "true" : "false");
- dd.addEventListener("mouseenter", () => sync(true));
- dd.addEventListener("mouseleave", () => sync(false));
- dd.addEventListener("focusin", () => sync(true));
+ const sync = () =>
+ trigger.setAttribute(
+ "aria-expanded",
+ dd.matches(":hover, :focus-within") ? "true" : "false",
+ );
+ dd.addEventListener("mouseenter", sync);
+ dd.addEventListener("mouseleave", sync);
+ dd.addEventListener("focusin", sync);
dd.addEventListener("focusout", () => {
- requestAnimationFrame(() => sync(dd.contains(document.activeElement)));
+ requestAnimationFrame(sync);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| dd.addEventListener("mouseenter", () => sync(true)); | |
| dd.addEventListener("mouseleave", () => sync(false)); | |
| dd.addEventListener("focusin", () => sync(true)); | |
| dd.addEventListener("focusout", () => { | |
| // focusout fires before focus lands on the next target; defer so we can | |
| // read whether focus is still inside this dropdown. | |
| requestAnimationFrame(() => sync(dd.contains(document.activeElement))); | |
| const sync = () => | |
| trigger.setAttribute( | |
| "aria-expanded", | |
| dd.matches(":hover, :focus-within") ? "true" : "false", | |
| ); | |
| dd.addEventListener("mouseenter", sync); | |
| dd.addEventListener("mouseleave", sync); | |
| dd.addEventListener("focusin", sync); | |
| dd.addEventListener("focusout", () => { | |
| // focusout fires before focus lands on the next target; defer so we can | |
| // read whether focus is still inside this dropdown. | |
| requestAnimationFrame(sync); | |
| }); |
The constraint
Structural and semantic accessibility only. Every change is invisible: colors, contrast, spacing, and layout render exactly as before. Color and contrast findings are deliberately left untouched per founder decision.
This was verified by pixel-diffing full-page screenshots before and after, at 1440 and 412 widths, across the landing, pricing, blog index, and a blog post.
Pixel-identity proof
Captures were stabilized by forcing
prefers-reduced-motion(so the scroll-driven "How it works" renders its deterministic static layout) and freezing CSS animations (the logo marquee).* The blog index initially differed only inside the fixed card frames, where
loading="lazy"hero images had decoded in one capture but not the other. Re-captured with all images deterministically decoded, both widths are byte-identical. To isolate this from the code change, the "before" blog index was rebuilt frommain(no<main>wrapper) and compared against the "after" build with the same image-load protocol.Lighthouse accessibility (landing, desktop)
Before: 87 → After: 96.
Failing audits resolved:
landmark-one-main— document had no<main>landmark.link-name— 20 logo-only marquee links had no accessible name.aria-prohibited-attr— 4 provider icon<span>s carriedaria-labelon a generic role.The single remaining Lighthouse failure is
color-contrast, which is out of scope by founder decision.What changed (structural only)
<main>landmark. InSite.astroit wraps the page<slot>; on the landing it wraps the section stack. The nav (<nav>) and footer (<footer>) were already landmarks. The<main>replaces no styled node and introduces no style, so margin-collapse and layout are preserved (proven by the byte-identical diffs).aria-label(GitHub, Slack, PostgreSQL, and so on). Duplicated marquee copies stayaria-hidden.aria-expanded="false". A small script now mirrors the CSS open state (:hover/:focus-within) onto the trigger'saria-expanded. Semantic only; the CSS still drives what is shown.role="img"on the four provider icon spans (makes their existingaria-labelvalid);titleon 9 YouTube iframes across 8 blog posts.Out of scope and untouched: all color and contrast findings.
langwas already present; the interactive islands (hosting toggle, category filter, template explorer) were already labeled.Screenshots
Before/after full-page captures and the pixel-diff artifacts are saved under
debug/website-a11y-qa/.https://claude.ai/code/session_013Qe1Vf2yvj33BVd5W1q7HB