wcag and responsiveness
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled

This commit is contained in:
2026-02-10 03:22:22 -05:00
parent 3b0b97f139
commit c21614020a
9 changed files with 569 additions and 8 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-10

View File

@@ -0,0 +1,88 @@
## Context
- The site is an Astro-based static site with shared global styling in `site/public/styles/global.css` and shared layout/navigation in `site/src/layouts/*`.
- Current UX gaps:
- Responsive behavior is inconsistent at smaller breakpoints (navigation does not collapse into a mobile-friendly menu).
- The background gradient shows abrupt cuts/banding on larger resolutions.
- Typography relies on system fonts; a smoother, display-friendly font is desired.
- Accessibility baseline is not formally enforced; target is WCAG 2.2 AA minimum standard (not necessarily 100% compliance).
## Goals / Non-Goals
**Goals:**
- Establish an explicit baseline of WCAG 2.2 AA-aligned behavior for the site shell and common interactive elements.
- Implement responsive layouts across common breakpoints; ensure primary navigation collapses into a hamburger menu with mild animation.
- Ensure the mobile menu is fully keyboard accessible and screen-reader friendly (correct semantics, labeling, focus management).
- Improve background rendering so gradients do not cut abruptly on large displays.
- Introduce a display-friendly font and apply it consistently across pages and components.
- Add lightweight verification (tests and/or build checks) that ensures the baseline remains intact.
**Non-Goals:**
- Full accessibility audit and remediation of all possible WCAG 2.2 AA items across all content (e.g., all third-party embeds, all user-provided HTML).
- Building a complete design system or replacing all visual styling.
- Implementing complex client-side routing or heavy JS frameworks.
## Decisions
1. Use a small client-side navigation controller for the hamburger menu
Why: Astro renders static HTML; a small, isolated script can provide toggling + focus management without adding framework complexity.
Alternatives considered:
- CSS-only checkbox hack: rejected (harder to manage focus/ARIA correctly, less robust).
- A full component framework (React/Vue): rejected (unnecessary weight).
2. Prefer semantic HTML + minimal ARIA
Why: Better interoperability across assistive technologies and less risk of incorrect ARIA.
Approach:
- Use a `<button>` to toggle the menu.
- Control a `<nav>` region (or a `<div>` wrapper) via `aria-controls` and `aria-expanded`.
- Ensure menu items remain standard links with predictable tab order.
3. Add explicit focus styles and reduced-motion support globally
Why: Focus visibility and motion preferences are core accessibility/usability requirements; implementing globally reduces drift.
Approach:
- Provide a consistent `:focus-visible` outline that meets contrast requirements.
- Wrap animations/transitions in `@media (prefers-reduced-motion: reduce)` fallbacks.
4. Fix gradient banding/cuts via a single, oversized background layer
Why: Multiple fixed-size radial gradients can show cutoffs on ultrawide/large viewports.
Approach:
- Render the background using a `body::before` fixed-position layer with large gradients and `inset: -40vmax` (or similar) to eliminate edges.
- Keep the existing aesthetic but adjust sizes/stops so it scales smoothly.
Alternatives considered:
- Using an image background: rejected (asset management, potential compression artifacts).
5. Use a webfont with good UI readability and a limited weight range
Why: Improve perceived polish while keeping performance predictable.
Approach:
- Choose a modern UI font family (e.g., `Inter`, `DM Sans`, `Manrope`, or `Space Grotesk`) with 2-3 weights.
- Prefer self-hosted font assets or a single external source with `font-display: swap`.
Decision will be finalized during implementation based on desired look and licensing.
## Risks / Trade-offs
- [Risk] Mobile menu introduces accessibility regressions (trap focus, broken escape handling)
-> Mitigation: implement standard patterns (toggle button, ESC closes, return focus, body scroll lock optional) and add tests for key attributes.
- [Risk] Global CSS changes affect existing layouts
-> Mitigation: keep changes scoped to site shell classes and add visual spot-checks for key pages (`/`, `/videos`, `/podcast`, `/blog`, `/about`).
- [Risk] Webfont increases page weight
-> Mitigation: limit to necessary weights, use `woff2`, preload critical fonts, and keep fallbacks.
## Migration Plan
1. Implement navigation collapse + hamburger toggle script and styles.
2. Add global focus-visible styling and reduced-motion fallbacks.
3. Fix background gradient rendering on large displays.
4. Add/replace typography stack and adjust headings/line-height as needed.
5. Add verification (tests / lint checks) and confirm responsive behavior on key pages.
Rollback:
- Revert navigation script + CSS changes to restore previous behavior.
## Open Questions
- Which specific webfont should be used (and will it be self-hosted or loaded via a provider)?
- Should the mobile menu lock body scroll while open (common pattern, but optional)?
- Should the menu close on route navigation (likely yes), and should it close on outside click (likely yes)?

View File

@@ -0,0 +1,29 @@
## Why
The site needs a more robust, state-of-the-art UX baseline: a minimum standard of WCAG 2.2 AA accessibility and a consistently responsive UI across devices and large displays.
## What Changes
- Establish a minimum accessibility baseline targeting WCAG 2.2 AA (without aiming for perfect/100% compliance).
- Make the UI fully responsive across common breakpoints (mobile/tablet/desktop/large desktop).
- Update the primary navigation to collapse into a hamburger menu on smaller viewports with mild animation.
- Fix the background gradient so it does not show abrupt cuts/banding at larger resolutions.
- Introduce a smoother, display-friendly font stack (and apply it consistently).
## Capabilities
### New Capabilities
- `wcag-responsive-ui`: Accessibility + responsive UI shell standards for layout, navigation, typography, and global styling (WCAG 2.2 AA baseline).
### Modified Capabilities
<!-- None expected; this change introduces a new UI-shell capability that affects multiple pages/components. -->
## Impact
- Frontend UI: `site/src/layouts/*`, header/navigation components, shared UI components, and global CSS (`site/public/styles/global.css`).
- Interaction patterns: keyboard navigation and focus styles, menu toggle behavior, and motion controls (respecting reduced-motion preferences).
- Visual design: typography and background rendering across large screens.
- Verification: add/update checks/tests for responsive nav behavior and basic accessibility expectations (e.g., menu toggle labeling, focus visibility).

View File

@@ -0,0 +1,67 @@
## ADDED Requirements
### Requirement: Responsive layout baseline
The site MUST be responsive across common breakpoints (mobile, tablet, desktop, and large desktop) and MUST not exhibit broken layouts (overlapping content, horizontal scrolling, clipped navigation).
#### Scenario: Mobile viewport does not horizontally scroll
- **WHEN** the site is viewed on a small mobile viewport
- **THEN** content reflows to a single-column layout and the page does not require horizontal scrolling to read primary content
#### Scenario: Large viewport uses available space without visual artifacts
- **WHEN** the site is viewed on a large desktop viewport (ultrawide / high resolution)
- **THEN** the background and layout scale without visible abrupt gradient cutoffs or banding artifacts
### Requirement: Collapsible primary navigation (hamburger menu)
The primary navigation MUST collapse into a hamburger menu on smaller viewports.
The menu toggle MUST be a `<button>` with:
- `aria-controls` referencing the menu container
- `aria-expanded` reflecting open/closed state
- an accessible label (e.g., `aria-label="Open menu"`/`"Close menu"` or equivalent)
When the menu is open, the menu items MUST be visible and keyboard navigable.
#### Scenario: Menu collapses on small viewport
- **WHEN** the viewport is below the mobile navigation breakpoint
- **THEN** the primary navigation renders in a collapsed state and can be opened via a hamburger toggle
#### Scenario: Menu toggle exposes accessible state
- **WHEN** the user toggles the menu open and closed
- **THEN** `aria-expanded` updates correctly and the toggle remains reachable via keyboard
### Requirement: Keyboard and focus behavior baseline (WCAG 2.2 AA aligned)
The site MUST support keyboard navigation for all primary interactive elements.
The site MUST provide visible focus indication for keyboard users using `:focus-visible` styles.
For the mobile menu:
- pressing `Escape` MUST close the menu (when open)
- closing the menu MUST return focus to the menu toggle button
#### Scenario: Focus is visible on links and buttons
- **WHEN** a keyboard user tabs through the page
- **THEN** the focused element shows a visible focus indicator
#### Scenario: Escape closes the menu
- **WHEN** the menu is open and the user presses `Escape`
- **THEN** the menu closes and focus returns to the menu toggle
### Requirement: Reduced motion support
The site MUST respect user motion preferences:
- if `prefers-reduced-motion: reduce` is set, animations/transitions for the menu and other UI elements MUST be reduced or disabled.
#### Scenario: Reduced motion disables menu animation
- **WHEN** the user's system preference is `prefers-reduced-motion: reduce`
- **THEN** opening/closing the menu does not use noticeable animation
### Requirement: Typography baseline (display-friendly font)
The site MUST use a display-friendly font stack consistently across pages, including headings and navigation.
The site MUST ensure text remains readable:
- reasonable line height
- sufficient contrast against the background for primary text and focus indicators
#### Scenario: Font is applied consistently
- **WHEN** a user navigates between pages
- **THEN** typography (font family and basic scale) remains consistent

View File

@@ -0,0 +1,27 @@
## 1. Navigation + Responsive Shell
- [x] 1.1 Identify the current header/nav implementation and decide the mobile breakpoint for collapsing navigation
- [x] 1.2 Implement hamburger toggle UI (button + icon) with correct ARIA (`aria-controls`, `aria-expanded`, accessible label)
- [x] 1.3 Implement the mobile menu panel styles + mild open/close animation (and close-on-route navigation)
- [x] 1.4 Add keyboard behavior: `Escape` closes menu and focus returns to toggle; ensure tab order remains sane
- [x] 1.5 Add reduced-motion fallback: disable/reduce menu animations when `prefers-reduced-motion: reduce`
- [x] 1.6 Ensure desktop navigation links remain clickable/accessible (no `inert`/`aria-hidden` desktop regression)
## 2. WCAG 2.2 AA Baseline
- [x] 2.1 Add/standardize global `:focus-visible` styles for links/buttons (high-contrast, consistent, not clipped)
- [x] 2.2 Ensure interactive elements meet minimum hit target expectations where feasible (spacing/padding for nav + key buttons)
- [x] 2.3 Add skip-to-content link and verify it is visible on focus and works across pages/layouts
- [x] 2.4 Audit and fix obvious contrast issues for primary text and focus outlines against the background
## 3. Background + Typography Polish
- [x] 3.1 Fix large-resolution background gradient cutoffs (move to a scaled, oversized background layer/pseudo-element)
- [x] 3.2 Introduce a display-friendly font (webfont) and apply consistently across the site; ensure sensible type scale/line-height
- [x] 3.3 Verify responsive behavior on key pages (`/`, `/videos`, `/podcast`, `/blog`, `/about`) at common breakpoints
## 4. Verification
- [x] 4.1 Add/update tests to ensure hamburger toggle ARIA attributes exist and update correctly
- [x] 4.2 Add/update tests or checks for focus-visible styling presence and reduced-motion rules
- [x] 4.3 Build the site and perform a keyboard-only smoke test (nav, cards, blog category nav, menu open/close)

View File

@@ -0,0 +1,71 @@
## Purpose
Define a minimum UX baseline for accessibility (WCAG 2.2 AA aligned) and responsive behavior for the site shell (navigation, focus, motion, typography, and background behavior).
## Requirements
### Requirement: Responsive layout baseline
The site MUST be responsive across common breakpoints (mobile, tablet, desktop, and large desktop) and MUST not exhibit broken layouts (overlapping content, horizontal scrolling, clipped navigation).
#### Scenario: Mobile viewport does not horizontally scroll
- **WHEN** the site is viewed on a small mobile viewport
- **THEN** content reflows to a single-column layout and the page does not require horizontal scrolling to read primary content
#### Scenario: Large viewport uses available space without visual artifacts
- **WHEN** the site is viewed on a large desktop viewport (ultrawide / high resolution)
- **THEN** the background and layout scale without visible abrupt gradient cutoffs or banding artifacts
### Requirement: Collapsible primary navigation (hamburger menu)
The primary navigation MUST collapse into a hamburger menu on smaller viewports.
The menu toggle MUST be a `<button>` with:
- `aria-controls` referencing the menu container
- `aria-expanded` reflecting open/closed state
- an accessible label (e.g., `aria-label="Open menu"`/`"Close menu"` or equivalent)
When the menu is open, the menu items MUST be visible and keyboard navigable.
#### Scenario: Menu collapses on small viewport
- **WHEN** the viewport is below the mobile navigation breakpoint
- **THEN** the primary navigation renders in a collapsed state and can be opened via a hamburger toggle
#### Scenario: Menu toggle exposes accessible state
- **WHEN** the user toggles the menu open and closed
- **THEN** `aria-expanded` updates correctly and the toggle remains reachable via keyboard
### Requirement: Keyboard and focus behavior baseline (WCAG 2.2 AA aligned)
The site MUST support keyboard navigation for all primary interactive elements.
The site MUST provide visible focus indication for keyboard users using `:focus-visible` styles.
For the mobile menu:
- pressing `Escape` MUST close the menu (when open)
- closing the menu MUST return focus to the menu toggle button
#### Scenario: Focus is visible on links and buttons
- **WHEN** a keyboard user tabs through the page
- **THEN** the focused element shows a visible focus indicator
#### Scenario: Escape closes the menu
- **WHEN** the menu is open and the user presses `Escape`
- **THEN** the menu closes and focus returns to the menu toggle
### Requirement: Reduced motion support
The site MUST respect user motion preferences:
- if `prefers-reduced-motion: reduce` is set, animations/transitions for the menu and other UI elements MUST be reduced or disabled.
#### Scenario: Reduced motion disables menu animation
- **WHEN** the user's system preference is `prefers-reduced-motion: reduce`
- **THEN** opening/closing the menu does not use noticeable animation
### Requirement: Typography baseline (display-friendly font)
The site MUST use a display-friendly font stack consistently across pages, including headings and navigation.
The site MUST ensure text remains readable:
- reasonable line height
- sufficient contrast against the background for primary text and focus indicators
#### Scenario: Font is applied consistently
- **WHEN** a user navigates between pages
- **THEN** typography (font family and basic scale) remains consistent

View File

@@ -8,6 +8,7 @@
--stroke: rgba(255, 255, 255, 0.16);
--accent: #ffcd4a;
--accent2: #5ee4ff;
--focus: rgba(94, 228, 255, 0.95);
}
* {
@@ -22,12 +23,10 @@ body {
body {
margin: 0;
color: var(--fg);
background:
radial-gradient(1000px 600px at 10% 10%, rgba(94, 228, 255, 0.22), transparent 55%),
radial-gradient(900px 600px at 90% 20%, rgba(255, 205, 74, 0.18), transparent 50%),
radial-gradient(900px 700px at 30% 90%, rgba(140, 88, 255, 0.14), transparent 55%),
linear-gradient(180deg, var(--bg0), var(--bg1));
background: linear-gradient(180deg, var(--bg0), var(--bg1));
/* Prefer a display-friendly font if available; fall back to system fonts. */
font-family:
"Manrope",
ui-sans-serif,
system-ui,
-apple-system,
@@ -41,11 +40,53 @@ body {
"Segoe UI Emoji";
}
/* Oversized fixed background layer to avoid gradient cutoffs on large screens. */
body::before {
content: "";
position: fixed;
inset: -40vmax;
z-index: -1;
pointer-events: none;
background:
radial-gradient(1200px 800px at 10% 10%, rgba(94, 228, 255, 0.22), transparent 60%),
radial-gradient(1100px 800px at 90% 20%, rgba(255, 205, 74, 0.18), transparent 58%),
radial-gradient(1200px 900px at 30% 90%, rgba(140, 88, 255, 0.14), transparent 62%);
}
a {
color: inherit;
text-decoration: none;
}
/* WCAG-ish baseline: make keyboard focus obvious. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 3px;
}
.skip-link {
position: absolute;
left: 14px;
top: 12px;
z-index: 999;
padding: 10px 12px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(10, 14, 28, 0.92);
color: var(--fg);
font-weight: 800;
transform: translateY(-220%);
transition: transform 140ms ease;
}
.skip-link:focus {
transform: translateY(0);
}
.container {
width: min(1100px, calc(100% - 48px));
margin: 0 auto;
@@ -78,10 +119,122 @@ a {
color: var(--muted);
}
.nav a {
padding: 10px 12px;
border-radius: 999px;
}
.nav a:hover {
color: var(--fg);
}
.nav-toggle {
display: none;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(255, 255, 255, 0.04);
color: var(--fg);
}
.nav-toggle-icon {
width: 18px;
height: 12px;
position: relative;
display: block;
}
.nav-toggle-icon::before,
.nav-toggle-icon::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 2px;
border-radius: 999px;
background: rgba(242, 244, 255, 0.92);
}
.nav-toggle-icon::before {
top: 0;
box-shadow: 0 5px 0 rgba(242, 244, 255, 0.92);
}
.nav-toggle-icon::after {
bottom: 0;
}
@media (max-width: 760px) {
.site-header {
position: sticky;
}
.nav-toggle {
display: inline-flex;
}
.nav {
position: absolute;
top: calc(100% + 10px);
right: 14px;
width: min(86vw, 320px);
padding: 10px;
display: flex;
flex-direction: column;
gap: 6px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.14);
background: rgba(10, 14, 28, 0.92);
box-shadow:
0 18px 60px rgba(0, 0, 0, 0.55),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
transform-origin: top right;
transition:
opacity 160ms ease,
transform 160ms ease,
visibility 0s linear 160ms;
}
.nav[data-open="false"] {
opacity: 0;
transform: translateY(-6px) scale(0.98);
pointer-events: none;
visibility: hidden;
}
.nav[data-open="true"] {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
visibility: visible;
transition:
opacity 160ms ease,
transform 160ms ease,
visibility 0s;
}
.nav a {
padding: 12px 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.001ms !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
}
}
.subnav {
display: flex;
gap: 10px;

View File

@@ -40,6 +40,14 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<!-- Display-friendly font (swap to avoid blocking render). -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/styles/global.css" />
{
@@ -49,6 +57,7 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
}
</head>
<body>
<a class="skip-link" href="#main-content">Skip to content</a>
<header class="site-header">
<a
class="brand"
@@ -60,7 +69,18 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
>
SanthoshJ
</a>
<nav class="nav">
<button
class="nav-toggle"
type="button"
aria-controls="primary-nav"
aria-expanded="false"
aria-label="Open menu"
data-nav-toggle
>
<span class="nav-toggle-icon" aria-hidden="true"></span>
</button>
<nav class="nav" id="primary-nav" data-open="false">
<a
href="/videos"
data-umami-event="click"
@@ -100,12 +120,79 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
</nav>
</header>
<main class="container">
<main class="container" id="main-content">
<slot />
</main>
<footer class="site-footer">
<p class="muted">© {new Date().getFullYear()} SanthoshJ</p>
<p class="muted">&copy; {new Date().getFullYear()} SanthoshJ</p>
</footer>
<script is:inline>
(() => {
const toggle = document.querySelector("[data-nav-toggle]");
if (!toggle) return;
const controlsId = toggle.getAttribute("aria-controls");
if (!controlsId) return;
const panel = document.getElementById(controlsId);
if (!panel) return;
const mql = window.matchMedia("(max-width: 760px)");
const setOpen = (open) => {
toggle.setAttribute("aria-expanded", open ? "true" : "false");
toggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
panel.dataset.open = open ? "true" : "false";
// Only hide/disable the nav panel in mobile mode. On desktop, nav is always visible/clickable.
if (mql.matches) {
panel.setAttribute("aria-hidden", open ? "false" : "true");
if (open) panel.removeAttribute("inert");
else panel.setAttribute("inert", "");
} else {
panel.removeAttribute("aria-hidden");
panel.removeAttribute("inert");
}
if (!open) toggle.focus({ preventScroll: true });
};
// Default state: closed on mobile, open on desktop.
setOpen(!mql.matches);
toggle.addEventListener("click", () => {
const isOpen = toggle.getAttribute("aria-expanded") === "true";
setOpen(!isOpen);
if (!isOpen) {
const firstLink = panel.querySelector("a");
if (firstLink) firstLink.focus({ preventScroll: true });
}
});
document.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
if (toggle.getAttribute("aria-expanded") !== "true") return;
setOpen(false);
});
panel.addEventListener("click", (e) => {
const t = e.target;
if (t && t.closest && t.closest("a")) setOpen(false);
});
document.addEventListener("click", (e) => {
if (toggle.getAttribute("aria-expanded") !== "true") return;
const t = e.target;
if (!t || !t.closest) return;
if (t.closest("[data-nav-toggle]")) return;
if (t.closest("#" + CSS.escape(controlsId))) return;
setOpen(false);
});
// If viewport changes, keep desktop usable and default mobile to closed.
mql.addEventListener("change", () => setOpen(!mql.matches));
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
async function read(rel: string) {
return await readFile(path.join(process.cwd(), rel), "utf8");
}
describe("wcag + responsive shell", () => {
it("adds skip link and main content anchor", async () => {
const src = await read("src/layouts/BaseLayout.astro");
expect(src).toContain('class="skip-link"');
expect(src).toContain('href="#main-content"');
expect(src).toContain('id="main-content"');
});
it("adds a hamburger toggle with ARIA and a script that updates aria-expanded", async () => {
const src = await read("src/layouts/BaseLayout.astro");
expect(src).toContain('class="nav-toggle"');
expect(src).toContain('aria-controls="primary-nav"');
expect(src).toContain('aria-expanded="false"');
expect(src).toContain('id="primary-nav"');
expect(src).toContain("toggle.setAttribute(\"aria-expanded\"");
expect(src).toContain("e.key !== \"Escape\"");
});
it("defines baseline focus-visible, reduced-motion, and oversized background layer", async () => {
const css = await read("public/styles/global.css");
expect(css).toContain("a:focus-visible");
expect(css).toContain("@media (prefers-reduced-motion: reduce)");
expect(css).toContain("body::before");
expect(css).toContain("@media (max-width: 760px)");
expect(css).toContain(".nav[data-open=\"true\"]");
});
});