Skip to main content

🧱 Part 16 — Accessibility First (Semantics, Keyboard, ARIA, Focus, Color)


Accessibility (a11y) is not optional: it helps keyboard users, screen‑reader users, and frankly everyone (think: low contrast screens, bright sun, fatigue). This part is beginner‑friendly and packed with commented patterns you can paste in today.


🎯 Goals

  • Semantic HTML & landmarks (nav, main, header, footer)
  • Keyboard support (Tab order, focus outlines, ESC to close)
  • Visible focus with :focus-visible
  • Skip link, visually hidden text, live regions
  • Readable colors (contrast), reduced motion
  • ARIA for dialog, tabs, and form errors

1) Baseline: semantic layout + skip link

Make the structure obvious to assistive tech and provide a quick jump to content.

// src/app/Layout.tsx
import { SkipLink } from '@/components/a11y/SkipLink';

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <SkipLink target="#main" />
      <header role="banner" className="p-4 border-b">
        <h1 className="text-xl font-semibold">TaskTimer</h1>
        <nav role="navigation" aria-label="Primary" className="mt-2">
          {/* real links here */}
        </nav>
      </header>
      <main id="main" role="main" className="p-4">
        {children}
      </main>
      <footer role="contentinfo" className="p-4 border-t text-sm">© TaskTimer</footer>
    </div>
  );
}
// src/components/a11y/SkipLink.tsx
export function SkipLink({ target = '#main' }: { target?: string }) {
  return (
    <a
      href={target}
      className="sr-only focus:not-sr-only fixed top-2 left-2 bg-black text-white px-3 py-2 rounded"
    >
      Skip to content
    </a>
  );
}

Add a minimal screen‑reader utility class:

/* src/styles/a11y.css */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.not-sr-only { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip: auto; white-space: normal; }

Include in index.css:

@import './styles/a11y.css';

2) Focus styles you can actually see

Never remove outlines. Style them.

/* src/styles/focus.css */
:root { --ring: #2563eb; --ring-offset: #ffffff; }
:root[data-theme="dark"] { --ring-offset: #0f172a; }

:focus { outline: none; }
:focus-visible { box-shadow: 0 0 0 3px var(--ring), 0 0 0 6px var(--ring-offset); border-radius: 8px; }

Include in index.css.


3) Accessible Button & Link reminders

  • Use <button> for actions, <a> with href for navigation.
  • Ensure name via visible text or aria-label.
  • Disable with disabled, not just CSS.

4) Dialog (Modal) — ARIA + focus trap + ESC

A modal must: announce itself, trap focus, restore focus on close, and close on ESC/backdrop.

// src/components/a11y/Modal.tsx
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

export function Modal({
  open,
  title,
  children,
  onClose,
  id = 'modal-1',
}: {
  open: boolean;
  title: string;
  children: React.ReactNode;
  onClose: () => void;
  id?: string;
}) {
  const ref = useRef<HTMLDivElement>(null);
  const lastActive = useRef<HTMLElement | null>(null);

  // Mount/unmount side effects
  useEffect(() => {
    if (!open) return;
    lastActive.current = (document.activeElement as HTMLElement) ?? null;
    // Focus the dialog on open
    ref.current?.focus();

    const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    // Prevent background scroll while open
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';

    return () => {
      document.body.style.overflow = prev;
      document.removeEventListener('keydown', onKey);
      // Restore focus to the trigger element
      lastActive.current?.focus();
    };
  }, [open, onClose]);

  // Simple focus trap (keeps Tab inside)
  useEffect(() => {
    if (!open) return;
    const el = ref.current!;
    const selectors = 'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])';
    const getNodes = () => Array.from(el.querySelectorAll<HTMLElement>(selectors)).filter(n => !n.hasAttribute('disabled'));
    const trap = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return;
      const nodes = getNodes();
      if (nodes.length === 0) return;
      const first = nodes[0];
      const last = nodes[nodes.length - 1];
      if (e.shiftKey && document.activeElement === first) { last.focus(); e.preventDefault(); }
      else if (!e.shiftKey && document.activeElement === last) { first.focus(); e.preventDefault(); }
    };
    el.addEventListener('keydown', trap);
    return () => el.removeEventListener('keydown', trap);
  }, [open]);

  if (!open) return null;

  return createPortal(
    <div
      aria-hidden={!open}
      className="fixed inset-0 bg-black/50 flex items-center justify-center p-4"
      onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div
        ref={ref}
        role="dialog"
        aria-modal="true"
        aria-labelledby={`${id}-title`}
        tabIndex={-1}
        className="bg-white text-black dark:bg-zinc-900 dark:text-white rounded-xl p-4 max-w-lg w-full"
      >
        <h2 id={`${id}-title`} className="text-lg font-semibold mb-2">{title}</h2>
        <div>{children}</div>
        <div className="mt-4 text-right">
          <button className="underline" onClick={onClose}>Close</button>
        </div>
      </div>
    </div>,
    document.body,
  );
}

Checks:

  • role="dialog" + aria-modal="true" + aria-labelledby.
  • Focus trap via keydown handler, ESC to close, backdrop click to close.
  • Restores focus to the opener.

5) Tabs — roving focus + ARIA roles

// src/components/a11y/Tabs.tsx
import { useId, useState } from 'react';

export function Tabs({ labels, children }: { labels: string[]; children: React.ReactNode[] }) {
  const [i, setI] = useState(0);
  const base = useId();

  return (
    <div>
      <div role="tablist" aria-label="Sample tabs" className="flex gap-2 border-b">
        {labels.map((label, idx) => (
          <button
            key={label}
            role="tab"
            id={`${base}-tab-${idx}`}
            aria-selected={i === idx}
            aria-controls={`${base}-panel-${idx}`}
            tabIndex={i === idx ? 0 : -1}
            className={i === idx ? 'font-semibold border-b-2' : 'text-gray-500'}
            onClick={() => setI(idx)}
            onKeyDown={(e) => {
              if (e.key === 'ArrowRight') setI((idx + 1) % labels.length);
              if (e.key === 'ArrowLeft') setI((idx - 1 + labels.length) % labels.length);
            }}
          >
            {label}
          </button>
        ))}
      </div>
      {children.map((node, idx) => (
        <div
          key={idx}
          role="tabpanel"
          id={`${base}-panel-${idx}`}
          aria-labelledby={`${base}-tab-${idx}`}
          hidden={i !== idx}
          className="py-3"
        >
          {node}
        </div>
      ))}
    </div>
  );
}

Checks: role="tablist", each tab has role="tab" with aria-controls → panel has role="tabpanel" with aria-labelledby. Arrow keys switch focus.


6) Forms — labels, errors, and hints

// src/components/forms/Field.tsx
type FieldProps = {
  id: string;
  label: string;
  hint?: string;
  error?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;

export function Field({ id, label, hint, error, ...props }: FieldProps) {
  const hintId = hint ? `${id}-hint` : undefined;
  const errId = error ? `${id}-error` : undefined;
  const describedBy = [hintId, errId].filter(Boolean).join(' ') || undefined;
  return (
    <div className="mb-3">
      <label htmlFor={id} className="block font-medium">{label}</label>
      <input
        id={id}
        aria-invalid={error ? 'true' : undefined}
        aria-describedby={describedBy}
        className={`mt-1 w-full rounded border px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'}`}
        {...props}
      />
      {hint && <div id={hintId} className="text-sm text-gray-500 mt-1">{hint}</div>}
      {error && <div id={errId} className="text-sm text-red-600 mt-1">{error}</div>}
    </div>
  );
}

Checks: label → input; aria-describedby chains hint + error; aria-invalid when invalid.


7) Announcements — live regions

For async success/error messages that appear dynamically.

// src/components/a11y/Live.tsx
export function LiveRegion({ message, polite = true }: { message: string; polite?: boolean }) {
  return (
    <div role="status" aria-live={polite ? 'polite' : 'assertive'} aria-atomic="true" className="sr-only">
      {message}
    </div>
  );
}

Use <LiveRegion message="Task saved" /> after a form submit.


8) Color contrast & motion

  • Pick colors that meet WCAG AA (use tooling: Stark, Accessible Brand Colors, or browser devtools).
  • Respect users who prefer less motion.
/* src/styles/reduced-motion.css */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

Include in index.css.


9) Linting & automated checks

  • ESLint plugin: eslint-plugin-jsx-a11y
  • Runtime hints (dev): @axe-core/react
pnpm add -D eslint-plugin-jsx-a11y @axe-core/react
// .eslintrc.cjs (excerpt)
module.exports = {
  plugins: ['jsx-a11y'],
  extends: ['plugin:jsx-a11y/recommended'],
};
// src/main.tsx (dev only)
if (import.meta.env.DEV) {
  import('@axe-core/react').then(({ default: axe }) => {
    axe(React, ReactDOM, 1000);
  });
}

✅ Wrap‑Up

You now have practical a11y scaffolding: semantic landmarks, visible focus, keyboard‑safe dialogs, ARIA‑correct tabs, form error wiring, live announcements, contrast and motion preferences — plus automated checks. Keep these patterns in your component library so accessibility is built‑in, not bolted‑on.

Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...