⚡ TL;DR: Most accessibility is free if you start with semantic HTML: a real button beats a div-button every time. Use the POUR principles, keyboard support, and contrast, and remember the first rule of ARIA is don't use ARIA.
Contents
- The button that isn't a button
- Accessibility is just usability, for everyone
- How assistive tech actually sees your page
- The four POUR principles
- Div-button vs. real button
- The first rule of ARIA
- Common mistakes that lock people out
- Takeaways
- Where to go next
The button that isn't a button
Here is a "button" that ships to production every day. It looks perfect. It has hover states, a nice gradient, and the click handler works:
DivButton.tsx
<div className="btn" onClick={handleCheckout}>
Checkout
</div>
Now try to use it without a mouse. Press Tab, your focus skips right past it, because a div is not focusable. Press Enter or Space, nothing happens, because a div has no built-in click behavior for the keyboard. Turn on a screen reader and it announces… nothing useful. It is just "Checkout, group." A blind user, a keyboard-only user, someone with a tremor using switch control, none of them can check out. You did not mean to lock them out. The markup did it for you.
📌 Who this is for: Frontend developers who can build a working UI but have never thought hard about who can't use it. No prior accessibility knowledge assumed. By the end you will know what to do, what not to do, and how to check your work.
Accessibility is just usability, for everyone
Accessibility is not a feature you bolt on for a minority. It is usability, for everyone, and most of it is free if you start with semantic HTML.
The web was born accessible. A plain HTML document with headings, links, and form controls works with a screen reader, a keyboard, voice control, and a braille display out of the box. We break that accessibility ourselves, by replacing <button> with <div>, by removing focus outlines because they "look ugly," by choosing grey-on-grey text that looks elegant in Figma. Accessibility work is mostly not breaking what the browser already gives you.
| In the real world | In tech |
|---|---|
| A curb cut (the ramp in a sidewalk) was built for wheelchairs | Semantic HTML was built for screen readers |
| But it also helps strollers, luggage, delivery carts, and cyclists | But it also helps keyboard users, voice control, SEO, and automated tests |
| Nobody complains the sidewalk has a ramp | Nobody complains your button is a real <button>
|
Accessibility helps the people you designed for too.
This is the curb-cut effect: design for the edge and you improve the center. Captions help deaf users and anyone watching on a muted phone in a quiet office. Good contrast helps low-vision users and anyone outside in sunlight. Keyboard support helps motor-impaired users and power users who never touch the mouse. Accessibility is not charity, it is good engineering with a wider blast radius.
How assistive tech actually sees your page
To reason about accessibility you need one mental model: the accessibility tree. When the browser parses your HTML it builds the DOM, and alongside it, a second, simplified tree describing what each element is and does: its role (button, link, heading), its name ("Checkout"), and its state (pressed, disabled, expanded). Screen readers and other assistive tech read that tree, not your CSS.
Semantic HTML feeds the accessibility tree, which is what assistive tech reads.
A <div onClick> shows up in the DOM, so it renders and clicks. But in the accessibility tree it has no role, no name, and no keyboard behavior, so the screen reader and keyboard paths simply have nothing to grab onto. A real <button> populates all three. That single difference is most of accessibility.
-
You write semantic HTML: Use the element that means what you intend: for actions, for navigation,
-
for structure, for inputs.
- The browser builds the accessibility tree: Each element gets a role, an accessible name, and state, derived automatically from the tag and its content.
- Assistive tech reads the tree: A screen reader announces "Checkout, button." The Tab key stops on it. Enter and Space activate it. You wrote none of that logic.
- The user acts: They hear what the control is, reach it with the keyboard, and operate it, exactly like a sighted mouse user, through a different door.
The four POUR principles
The Web Content Accessibility Guidelines (WCAG) organize everything under four principles, abbreviated POUR. You do not need to memorize the spec, you need the gist of what each one asks of your UI.
| Principle | The question it asks | In practice |
|---|---|---|
| Perceivable | Can users sense the content? | Alt text on images, captions on video, enough color contrast, never rely on color alone to convey meaning |
| Operable | Can users drive the UI? | Everything works by keyboard, focus is visible, no keyboard traps, enough time to act, no seizure-inducing flashes |
| Understandable | Can users make sense of it? | Clear labels, predictable behavior, helpful error messages, a declared page language |
| Robust | Will it work with their tools? | Valid HTML and correct roles/names/states so any browser or assistive tech can interpret it |
POUR, what each principle means at the keyboard.
💡 A pocket test: Unplug your mouse and use your app with the keyboard alone. Then zoom the browser to 200%. Those two checks surface the majority of Operable and Perceivable failures in minutes.
Div-button vs. real button
Let us fix the opening example. The naive instinct is to patch the div until it behaves like a button. Watch how much code that takes, and how much the right element gives you for free.
Inaccessible.tsx
// Inaccessible: a div pretending to be a button
<div className="btn" onClick={handleCheckout}>
Checkout
</div>
// "Fixing" the div means re-implementing the browser:
<div
className="btn"
role="button" // 1. fake a role
tabIndex={0} // 2. make it focusable
onClick={handleCheckout}
onKeyDown={(e) => { // 3. wire up keyboard yourself
if (e.key === "Enter" || e.key === " ") handleCheckout();
}}
>
Checkout
</div>
Accessible.tsx
// Accessible: just use the element that already does all of that
<button className="btn" onClick={handleCheckout}>
Checkout
</button>
// Focus is visible by default, keep it, style it, never delete it:
.btn:focus-visible {
outline: 2px solid #0d9488; /* teal */
outline-offset: 2px;
}
// Icon-only button? Give it an accessible NAME or it announces nothing:
<button onClick={handleClose} aria-label="Close dialog">
<XIcon aria-hidden="true" />
</button>
The <button> is focusable, keyboard-operable, and announced as "Checkout, button", with zero extra code. Two things worth calling out: never delete the focus outline (use :focus-visible to style it instead of removing it), and an icon-only button needs an accessible name via aria-label, because there is no visible text for the accessibility tree to use. Mark the decorative icon aria-hidden="true" so it is not announced twice.
The first rule of ARIA
The first rule of ARIA is: don't use ARIA. If a native HTML element gives you the role and behavior you need, use it instead.
WCAG ARIA Authoring Practices
ARIA, Accessible Rich Internet Applications, is a set of role, aria-*, and state attributes that describe elements to assistive tech. The catch: ARIA changes only what is announced, never how the element behaves. Adding role="button" to a div makes a screen reader say "button," but it does not make Tab stop on it, and it does not make Enter fire the handler. You still have to build all of that yourself, and forget one piece, and you have shipped a control that claims to be a button but is broken. A real <button> is honest by construction.
So when is ARIA the right tool? When HTML genuinely has no element for what you are building: a tabbed interface, a custom autocomplete combobox, a tooltip, a aria-live region that announces async updates. There ARIA is essential, but follow a recipe (the ARIA Authoring Practices patterns), do not improvise. The rule of thumb: reach for the HTML element first, and only add ARIA when you have proven nothing native fits.
⚠️ Bad ARIA is worse than no ARIA: An incorrect role or a stale aria-state actively lies to screen-reader users.
<button role="link">or anaria-expandedthat never updates is more confusing than plain markup. If you are not sure the ARIA is correct and kept in sync, leave it off.
Common mistakes that lock people out
-
Div buttons and div links.
<div onClick>is not focusable or keyboard-operable. Use<button>for actions and<a href>for navigation, the choice signals intent to the accessibility tree. -
Deleting focus styles.
outline: nonewith no replacement leaves keyboard users with no idea where they are. Style:focus-visibleinstead, never remove it. - Low color contrast. Grey-on-white that looks sleek can fail WCAG. Body text needs a contrast ratio of at least 4.5:1 (3:1 for large text). Check it with your DevTools or a contrast tool.
- Color as the only signal. "Required fields are red" is invisible to colorblind users. Pair color with an icon, text, or an asterisk.
-
Images without alt text. Every meaningful
<img>needsaltdescribing its content. Decorative images getalt=""(empty, not missing) so screen readers skip them. -
Misused or stale ARIA. Inventing roles, or an
aria-expanded/aria-pressedthat never updates, lies to users. Prefer native HTML; keep any ARIA state in sync with reality. -
Inputs with no label. A placeholder is not a label, it vanishes on typing and is poorly announced. Wire every input to a real
<label>(oraria-label).
Takeaways
The whole article in seven lines
- Accessibility is usability for everyone, and mostly free if you start with semantic HTML.
- Assistive tech reads the accessibility tree (role · name · state), not your CSS.
- Use the right element:
<button>for actions,<a href>for links,<label>for inputs, headings for structure. - POUR: Perceivable, Operable, Understandable, Robust, the four questions every UI must answer.
- Keep focus visible (
:focus-visible), hit 4.5:1 contrast, and never use color as the only signal. - First rule of ARIA: don't use ARIA, it changes announcements, not behavior. Reach for HTML first.
- Test it: unplug the mouse, tab through, zoom to 200%, run a screen reader.
Where to go next
Accessibility is a thread that runs through everything else on the Frontend Engineer path, it is not a separate module so much as a quality bar for all the UI you build. Two sibling articles connect directly to it.
-
CSS Fundamentals: The Box Model & Layout, where you will style
:focus-visibleoutlines and make sure contrast survives your color palette. -
Component Architecture & Design Systems, build accessibility into your shared
Button,Input, andDialogcomponents once, and every screen inherits it.
Then practice the pocket test on your own app: unplug the mouse, tab through a full flow, and fix the first thing that traps or hides your focus. That one habit will teach you more than any checklist.
Originally published on TheSimplifiedTech, where this guide is interactive, with in-browser terminal labs and diagrams. Learn cloud and DevOps by doing, no videos.
Top comments (0)