Why? Makes apps usable by everyone and meets WCAG standards.
Semantic markup lets browsers, screen-readers, and assistive tech understand your UI without extra hints. Sticking to native elements first—and adding ARIA only when absolutely required—delivers built-in keyboard support, predictable focus order, and superior SEO, while shrinking the surface for accessibility bugs.
// Accessible toggle component (TypeScript + Tailwind)
import { useState } from "react";
export function DarkModeToggle() {
const [on, setOn] = useState(false);
return (
<button
type="button"
aria-pressed={on}
onClick={() => setOn(!on)}
className={`flex items-center gap-2 rounded-md px-3 py-2
${on ? "bg-zinc-900 text-white" : "bg-zinc-100"}`}
>
<span className="sr-only">Toggle dark mode</span>
<svg
aria-hidden // decorative icon
className="h-5 w-5"
focusable="false"
viewBox="0 0 20 20"
>
{/* …sun/moon path… */}
</svg>
<span>{on ? "On" : "Off"}</span>
</button>
);
}
Why it’s good:
<button>
element (built-in keyboard + focus).aria-pressed
(standard, no custom role).aria-hidden
, while the visible label is duplicated for sighted users.// ⚠️ Mouse-only “button” that breaks accessibility
export function DarkModeToggle() {
const [on, setOn] = useState(false);
return (
<div
onClick={() => setOn(!on)}
className={`cursor-pointer select-none rounded-md px-3 py-2
${on ? "bg-zinc-900 text-white" : "bg-zinc-100"}`}
>
{/* No semantic role, no keyboard handlers, no aria state */}
<svg className="h-5 w-5" viewBox="0 0 20 20">
…
</svg>
{on ? "On" : "Off"}
</div>
);
}
What’s wrong:
<div>
with onClick
has no keyboard or focus support.role="button"
& tabIndex=0
; still falls short of native behavior (spacebar activation, aria-pressed
, etc.).<button>
, <a>
, <input>
) over generic <div>
/<span>
wrappers.aria-hidden
for purely decorative icons; supply off-screen text (.sr-only
) for visual-only labels.