---
title: "31 — Accessibility — Semantics That Actually Reach Everyone"
uid: accessibility
tags: ["accessibility", "a11y", "pwa", "aria", "roadmap:frontend", "inclusive-design"]
excerpt: "A11y is just semantics done well, made universal — the same 'describe what this content is' habit, plus the question of whether the meaning actually reaches assistive tech."
date: 2026-08-12T18:35:06+0000
source: https://www.aveshina.my.id/en/blog/accessibility
---

"A checklist you run at the end, before ship" was how I treated accessibility, which made it an afterthought instead of a property. The line that everything else hangs off: **accessibility ("a11y") is just semantics done well, made universal.** It's the same "describe what this content *is*" habit from the HTML notes — but now the question is whether that meaning actually *reaches* someone using a screen reader, a keyboard, or a low-vision setting, not just my own eyeballs [1].

The a11y and the PWA sibling in one frame: a11y is "reach everyone *as a person*" (meaning delivered through the right tags, contrast, keyboard support, and ARIA (Accessible Rich Internet Applications — attributes for when HTML alone can't express the meaning) where the semantics aren't enough); a Progressive Web App is "reach everyone *on any device*" (a web app that's installable and works offline, app-like, via a service worker and a manifest). Both are the same instinct — stop assuming one default user on one default machine.

```figure
<svg viewBox="0 0 740 290" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two siblings of reach. Center: a box labelled Semantic HTML — meaning declared once. Two arrows fan out. Top arrow: Accessibility (a11y) — the same meaning delivered to assistive tech through alt text, keyboard focus, color contrast, landmarks, and ARIA. Bottom arrow: Progressive Web App (PWA) — the same app delivered to any device, installable and offline, via service worker and manifest. Caption: one source of meaning — two kinds of reach.">
  <defs>
    <marker id="aarrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- source -->
    <rect x="40" y="110" width="180" height="80" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="130" y="142" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Semantic HTML</text>
    <text x="130" y="160" font-size="10.5" fill="#475569" text-anchor="middle">meaning declared once</text>
    <text x="130" y="176" font-size="10.5" fill="#475569" text-anchor="middle">in the markup</text>

    <!-- a11y sibling -->
    <rect x="490" y="40" width="210" height="78" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="595" y="66" font-size="13" font-weight="700" fill="#422006" text-anchor="middle">Accessibility (a11y)</text>
    <text x="595" y="85" font-size="10" fill="#475569" text-anchor="middle">meaning delivered to assistive tech</text>
    <text x="595" y="100" font-size="10" fill="#64748b" text-anchor="middle">alt · keyboard · contrast · ARIA</text>

    <!-- pwa sibling -->
    <rect x="490" y="178" width="210" height="78" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="595" y="204" font-size="13" font-weight="700" fill="#052e16" text-anchor="middle">Progressive Web App</text>
    <text x="595" y="223" font-size="10" fill="#475569" text-anchor="middle">app delivered to any device</text>
    <text x="595" y="238" font-size="10" fill="#64748b" text-anchor="middle">service worker · manifest · offline</text>

    <path d="M220,140 C360,140 400,80 488,80" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aarrow)"/>
    <path d="M220,160 C360,160 400,218 488,218" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#aarrow)"/>

    <text x="370" y="276" font-size="11" fill="#64748b" text-anchor="middle" font-style="italic">one source of meaning — two kinds of reach</text>
  </g>
</svg>
```

## Why a11y is "semantics done well"

A screen reader doesn't look at pixels. It reads the meaning the tags declare [1]. So if I describe the meaning correctly — one <h1> for the page's top-level heading, <h2>/<h3> for a logical outline, <main>/<nav>/<aside> as landmarks (named regions a screen reader can jump between) — a blind user navigates my page like a table of contents: jump to landmarks, skim the heading outline, skip the chrome they've heard a hundred times. That's not a separate accessibility layer. That's the HTML notes, applied seriously.

The failures I had to unlearn are all the same mistake: **using meaning-free markup where meaning was needed.**

- A <div onclick> instead of <button> — the <div> isn't focusable, isn't keyboard-operable, and announces nothing to assistive tech. A <button> is all three by default [2].
- Skipping heading levels for visual effect (<h1> straight to <h4>) — breaks the outline someone navigates by.
- An image with no alt — the screen reader either reads the filename or says nothing, so the meaning never arrives [3].

The fix is never "add an accessibility library." It's "use the right element." The right tag is the accessibility.

## The four things semantics alone can't cover

Get the semantics right and a lot of a11y comes free. But meaning doesn't cover everything. Four things still need deliberate attention [1][3]:

- **Alt text.** Every content <img> needs alt describing its purpose — short for decoration, richer when the image carries information. Decorative images get alt="" (empty, not omitted) so they're explicitly skipped, not announced as a mystery.
- **Keyboard navigation.** Can I reach every interactive thing with Tab, and operate it with the expected keys (Enter/Space on buttons, arrows on tabs)? If a control is mouse-only, a keyboard user is locked out.
- **Focus.** The focus ring isn't ugly — it's the keyboard user's cursor. Hiding :focus globally (or worse, outline: none) without a replacement strands keyboard users with no idea where they are. :focus-visible lets me style the ring only for keyboard users, leaving mouse users clean.
- **Color contrast.** Text needs enough contrast against its background (WCAG AA — the Web Content Accessibility Guidelines, level AA — targets roughly 4.5:1 for body text). Low-contrast grey-on-white looks refined and reads as nothing for low-vision users [3].

These aren't a11y *features* — they're the last mile of "does the meaning actually reach the person."

## ARIA: the gap-filler, not the default

There's a temptation, once you learn ARIA exists, to slap role="button" on every <div> and call it accessible. That's backwards. **The first rule of ARIA is: don't use ARIA if a native element exists** [4]. A real <button> gives you focus, keyboard handling, and the correct role for free. An ARIA-role="button" <div> gives you the label and leaves you to rebuild everything else by hand — and one missed key handler makes it worse than the plain <div>.

Where ARIA *does* earn its place is where HTML genuinely has no element for the thing: a custom disclosure widget, a live region that announces updates, or an expanded/collapsed state that needs to be told to assistive tech explicitly. aria-label, aria-expanded, aria-live, aria-describedby — these fill gaps the markup can't express on its own. The mental rule I keep: native first, ARIA for what native can't say.

## Screen readers are the honest reader

The fastest way I found to stop guessing about a11y is to actually turn on VoiceOver (macOS) or NVDA (Windows) and try to use my own page without looking. Five minutes of that exposed more assumptions than any article: a form field with no <label> announced as "edit text, blank"; a nav built from <div>s read as an undifferentiated wall; a modal (popup dialog) that trapped the cursor because I never sent focus into it. Screen readers aren't a separate audience reading something different — they're reading the *same* document my markup describes, and they show me, plainly, where my description was wrong [5].

## PWA — reach everyone *on any device*

If a11y is "reach every *person*," a Progressive Web App is "reach every *device*" — and it's the natural sibling, because both come from the same instinct: don't assume one default client [6]. A PWA is a regular web app (HTML, CSS, JavaScript) that, with two additions, behaves like a native app: **installable** to the home screen, and **offline-capable**.

The two additions [6][7]:

- **Web App Manifest** — a small JSON file (manifest.json) that tells the OS how to install the app: its name, icons, start URL, theme color, and display mode (standalone hides the browser chrome so it looks like a native app).
- **Service Worker** — a JavaScript file the browser runs *separately from the page*. It sits between the page and the network and can intercept requests, serving cached responses when offline. That's what makes "open the app with no signal" actually work — the service worker has the shell cached.

The payoff is the reach a native app can't match. One URL works on desktop, mobile, installable on iOS, installable on Android, indexed by search, linkable, and functional offline — no app store, no review queue, no binary download. The web's strengths (URLs, no install gate) plus the app's strengths (offline, home-screen presence) in one delivery.

## How I use this

The habit these notes left me with is a single question added to my default reach: **when I write a tag, I ask not just *what is this content?* but *does its meaning reach someone who isn't me?*** That's alt text, a real <button>, a kept focus ring, a passing contrast check — almost all of it free if the semantics are right. ARIA only when native can't say it. And on the delivery side, when I want the app to *arrive* on a device rather than just be visited in a tab, I reach for the manifest + service-worker pair instead of a native build.

That's the whole frame: describe meaning well, then make sure it reaches — as a person, and on a device.

## References

[1] Google, "Accessibility for developers," web.dev, 2023. [Online]. Available: [https://web.dev/accessibility](https://web.dev/accessibility)

[2] Google, "Use semantic HTML," web.dev, 2023. [Online]. Available: [https://web.dev/articles/semantic-html](https://web.dev/articles/semantic-html)

[3] Mozilla, "What is accessibility?," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/What_is_accessibility)

[4] W3C Web Accessibility Initiative, "ARIA Authoring Practices Guide — No. 1 rule," WAI-ARIA, 2024. [Online]. Available: [https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/](https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/)

[5] Mozilla, "Screen readers," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Screen_readers](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Screen_readers)

[6] Google, "Learn PWA," web.dev, 2024. [Online]. Available: [https://web.dev/learn/pwa/](https://web.dev/learn/pwa/)

[7] Mozilla, "Progressive Web Apps," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/)

```quiz
Q: Which best describes what accessibility ("a11y") actually is?
- A separate layer of tooling bolted on before ship
- Semantics done well, made universal so meaning reaches assistive tech
- A legal compliance checkbox unrelated to your markup
correct: 1
explain: a11y is the same "describe what this content is" habit as semantic HTML, applied seriously so the meaning reaches screen readers, keyboard users, and low-vision users — not a separate layer.

Q: You need a clickable "Submit" action on a div. The most accessible fix is…
- add role="button" and tabindex="0" to the div
- replace it with a native <button>
- add aria-label to the div
correct: 1
explain: The first rule of ARIA is: don't use ARIA if a native element exists. A real <button> gives focus, keyboard handling, and the correct role for free — no ARIA can fully match it.

Q: An image that is purely decorative should use…
- no alt attribute at all
- alt="" (empty)
- alt="decorative image"
correct: 1
explain: An empty alt explicitly tells assistive tech to skip it. Omitting alt makes the reader announce the filename or "image"; an empty string is the deliberate "this adds nothing" signal.

Q: What two additions turn a regular web app into a Progressive Web App?
- A build step and a native bridge
- A Web App Manifest and a Service Worker
- HTTPS and a CSS reset
correct: 1
explain: The manifest tells the OS how to install the app (name, icons, display mode); the service worker intercepts requests and serves cached responses, which is what makes offline work possible.

Q: Why is hiding :focus globally (e.g. outline: none) an accessibility problem?
- It isn't — focus rings are visual noise
- It removes the keyboard user's cursor, stranding them with no idea where they are
- It breaks screen readers entirely
correct: 1
explain: The focus ring is the keyboard user's equivalent of the mouse cursor. Remove it and they can't see what Tab has selected. Use :focus-visible to style it only for keyboard users instead.
```
