Skip to content
Astro best practices 4 min read

Accessibility Best Practices

Accessibility (often shortened to a11y) is the practice of building sites everyone can use, including people who rely on screen readers, keyboard navigation, or other assistive technology. Astro is a strong foundation for accessible work because it ships zero JavaScript by default and renders real, server-rendered HTML — assistive tech receives meaningful markup instead of an empty <div> waiting for hydration. The work that remains is mostly discipline: write semantic HTML, keep interactivity keyboard-operable, and reach for ARIA only when native elements fall short.

Start with semantic HTML

The single highest-leverage accessibility decision is choosing the right element. Native HTML elements come with built-in roles, keyboard behavior, and focus handling that you would otherwise have to recreate by hand. A <button> is focusable, fires on Enter and Space, and announces itself as a button — a <div onclick> does none of that.

Because Astro components are just HTML with a script fence, there is nothing special to learn: write the markup you would write in a plain .html file.

---
// src/components/ArticleCard.astro
const { post } = Astro.props;
---
<article>
  <h2>
    <a href={`/blog/${post.slug}/`}>{post.title}</a>
  </h2>
  <p>{post.excerpt}</p>
  <time datetime={post.date.toISOString()}>
    {post.date.toLocaleDateString()}
  </time>
</article>

Use landmarks (<header>, <nav>, <main>, <footer>) so screen reader users can jump between regions, and maintain a logical heading order — one <h1> per page, no skipped levels.

Tip: Prefer native elements over ARIA. The first rule of ARIA is “don’t use ARIA” — a real <button>, <a href>, or <details> is more robust than any role you bolt on.

Every meaningful image needs descriptive alt text; decorative images should use an empty alt="" so screen readers skip them. Astro’s <Image /> component requires alt, which nudges you toward doing this correctly.

---
import { Image } from 'astro:assets';
import hero from '../assets/team.jpg';
---
<Image src={hero} alt="The DevCraftly engineering team at a whiteboard" />
<img src="/divider.svg" alt="" role="presentation" />

For forms, associate every input with a <label> and group related controls with <fieldset>/<legend>:

<form action="/subscribe" method="post">
  <fieldset>
    <legend>Newsletter signup</legend>
    <label for="email">Email address</label>
    <input id="email" name="email" type="email" required autocomplete="email" />
  </fieldset>
  <button type="submit">Subscribe</button>
</form>

Links should make sense out of context — avoid “click here.” Screen reader users often navigate by a list of links, so the text alone must convey the destination.

Keyboard and focus management

Anything a mouse user can do, a keyboard user must be able to do too. Native interactive elements are focusable by default; if you build a custom widget, manage focus explicitly and never trap it.

A common pattern is a “skip to content” link so keyboard users can bypass repeated navigation:

---
// src/layouts/BaseLayout.astro
---
<html lang="en">
  <body>
    <a href="#main" class="skip-link">Skip to main content</a>
    <header><!-- nav --></header>
    <main id="main">
      <slot />
    </main>
  </body>
</html>

<style>
  .skip-link {
    position: absolute;
    left: -999px;
  }
  .skip-link:focus {
    left: 1rem;
    top: 1rem;
  }
</style>

Always preserve a visible focus indicator. If you reset outlines for design reasons, replace them — never remove focus styling outright.

:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

Accessible interactive islands

When a feature genuinely needs JavaScript, isolate it in an island and hydrate it with the appropriate client:* directive. Keep islands small and ensure they render an accessible baseline before hydration, so the experience degrades gracefully.

---
import Disclosure from '../components/Disclosure.astro';
import Menu from '../components/Menu.jsx';
---
<!-- Native disclosure: zero JS, fully accessible -->
<Disclosure summary="Shipping details">
  <p>Orders ship within two business days.</p>
</Disclosure>

<!-- Framework island hydrated only when visible -->
<Menu client:visible />

A <details>/<summary> disclosure needs no JavaScript and is keyboard-accessible for free:

---
const { summary } = Astro.props;
---
<details>
  <summary>{summary}</summary>
  <div><slot /></div>
</details>

For richer widgets that do need ARIA, follow the APG patterns precisely — wiring aria-expanded, aria-controls, and roving tabindex correctly, including Escape and arrow-key handling.

ConcernNative solutionWhen you need JS/ARIA
Toggle content<details>/<summary>Animated accordion with aria-expanded
Dialog<dialog> elementCustom modal needing focus trap + aria-modal
Tooltiptitle attributeRich tooltip with aria-describedby
Navigation<nav> + <a>Dropdown menu following APG menu pattern

Testing and auditing

Astro can surface issues at build time. The @astrojs/lighthouse integration and tools like axe-core catch many problems automatically, but automated checks find only a fraction of issues — manual keyboard and screen reader testing is essential.

npm install -D eslint-plugin-jsx-a11y axe-core
npx @lhci/cli autorun

Output:

✓ Running Lighthouse 3 time(s) on http://localhost:4321/
  Accessibility: 100
  Best Practices: 100

Tab through every page, confirm focus order is logical, and verify color contrast meets WCAG AA (4.5:1 for body text).

Best practices

  • Reach for semantic HTML first; use ARIA only to fill genuine gaps native elements can’t cover.
  • Require descriptive alt on meaningful images and alt="" on decorative ones.
  • Set lang on <html>, use landmarks, and keep a single logical heading hierarchy.
  • Guarantee full keyboard operability and a visible :focus-visible indicator on every interactive element.
  • Keep JS in small islands, render an accessible baseline server-side, and hydrate with the lightest client:* directive.
  • Test with the keyboard and a screen reader, not just automated scanners.
  • Verify color contrast against WCAG AA before shipping.
Last updated June 14, 2026
Was this helpful?