# How to Build an Accessible Website in Next.js (Beginners)

> Cara membuat website ramah aksesibilitas Next.js for beginners: semantic HTML, labels, skip link, focus ring, contrast, reduced motion and quick checks.

URL: https://www.matthewswong.com/en/blog/cara-membuat-website-accessible-nextjs/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-10-17
- Updated: 2026-10-17
- Category: Web Dev

> **Key takeaway:** To build an accessible website in Next.js: use semantic HTML such as main, button and label, keep keyboard focus clearly visible, add a skip link, measure text contrast at 4.5:1 or better, and honour prefers-reduced-motion. Then test with the Tab key, ESLint, Lighthouse and axe-core.

You press Tab on the page you just built and focus jumps somewhere unexpected. Or the card button you made from a div works with a mouse but a keyboard cannot reach it. Or a screen reader says only the word input, without saying what the field is for. Almost every beginner gets stuck on one of these three, and none of them show on screen when you test with a mouse.

This guide builds a keyboard-friendly page in seven steps. The numeric rules (4.5:1 and 3:1) were checked against the W3C page, the label wiring against MDN, and the test commands against the Next.js, Lighthouse and axe-core documentation. Two snippets, the skip link and the focus rule, are simplified versions of the code on this site, and every other example uses placeholder names.

Want the theory first, including the WCAG criteria behind each step? Follow the free lesson

Web Accessibility Basics for Beginners in a Next.js Site

in the Web app skills track. This post is the step-by-step practice.

What you need

A Next.js project using the App Router (the app folder) and TypeScript.

One page that already has links, buttons and a form, so there is something to fix.

Node.js and a terminal, to run ESLint and Lighthouse from the command line.

A desktop browser and a keyboard. Put the mouse aside while you test.

## Step 1: Start from semantic HTML elements

A native element carries its own meaning. Without header, nav, main and footer, and headings in order, a page reads to a screen reader as one long block. Compare these two versions.

In Next.js this is cheap: one main in the layout wrapping children, one h1 per page, then h2 and h3 in order. Give every page a unique title too. The Next.js documentation explains that its route announcer reads document.title first, then the h1 element, then the URL pathname, to tell assistive technology users that the page changed.

## Step 2: Replace clickable divs with a button

This is the most common mistake. A div with onClick is not in the Tab order, does not respond to Enter or Space, and has no role. A real button element gives you all three for free, and its visible text automatically becomes its name.

Because onClick needs interaction in the browser, this component must be a Client Component. Keep only the small button as a Client Component, not the whole page. The splitting rule is explained in

the difference between Server and Client Components in Next.js

. One more rule of thumb: a link to go to another page, a button to perform an action.

## Step 3: Label every input and give images alternative text

A label is connected to its input programmatically, not just placed near it. Per MDN, you make the connection by putting an id on the input and giving the label's for attribute the same value, and clicking the label moves focus to the input. In JSX, the for attribute is written htmlFor. A placeholder is not a substitute: its hint disappears once the user starts typing, so keep a visible label.

An image that carries information needs alt text describing its content, not a word like image or photo. The Next.js documentation notes that eslint-plugin-jsx-a11y helps make sure img elements have alt text, so this gap can be caught early.

## Step 4: Add a skip link and keep focus visible

A keyboard user has to Tab through the whole header on every page. A skip link is the first link on the page and jumps focus to the main content. The version below is simplified from this site's SiteChrome component.

A skip link whose href is #main-content, with no element carrying the id main-content, does nothing visible. Keep the link and its target close together, then press Tab and Enter on a real page to confirm focus actually moves.

Next, the focus ring. The common failure is deleting the outline because it looks untidy. Restyle it instead of deleting it. One global :focus-visible rule gives every element a clear ring, and a given component can refine it.

This site uses a thick 3 pixel outline with a 2 pixel offset for every element. Choose a ring colour that contrasts with your page background, and check it in the next step.

## Step 5: Measure colour contrast, do not guess

According to the W3C explanation of criterion 1.4.3 (level AA), text needs a contrast ratio of at least 4.5:1, and large-scale text at least 3:1. Large-scale text starts at 18 point, or 14 point when bold. On a dark theme with translucent layers, the effective background is a blend, so judging by eye is often wrong.

Measure the final rendered colours with a contrast checker, available in browser developer tools and in many free tools, and measure again whenever a colour token changes. W3C also notes the ratios are thresholds that must not be rounded: 4.499:1 does not meet 4.5:1. This guide deliberately quotes no ratio for any particular palette, because a number only becomes a claim once somebody has measured it.

## Step 6: Honour prefers-reduced-motion

Some visitors ask their operating system to reduce animation. MDN explains that the value reduce means the visitor has asked for an interface that removes, reduces or replaces motion-based animation. In CSS, use a media query. In JavaScript, ask the same media query through window.matchMedia before starting any scripted animation.

Reduced motion does not mean reduced content. Replace movement with an instant change or a fade, and keep every piece of text, caption and control. The two snippets above are simplified versions of the pattern the interactive explainers on this site use.

## Step 7: Test with the keyboard, ESLint, Lighthouse and axe-core

Start with the cheapest test: put the mouse down, click the address bar, then keep pressing Tab through the whole page. The skip link should appear on the first Tab, every control should show a ring, and nothing clickable should be skipped. After that, run these three commands.

Mind the limits. Next.js ships eslint-plugin-jsx-a11y and its documentation lists example rules such as aria-props and role-has-required-aria-props, so do not assume ESLint catches a div with onClick. That is why the grep command above still earns its place. Lighthouse runs the accessibility category, and its README documents the --only-categories and --view flags.

The axe-core README says it finds on average 57 percent of WCAG issues automatically, and flags elements as incomplete when it is unsure and a manual review is needed. So a green score is not proof that your page is accessible. Listen to your page with a screen reader at least once, and do not treat the result as a certificate or a legal guarantee.

Common errors and how to fix them

Seven problems that most often appear when a beginner first makes a page accessible.

Symptom

Cause

Fix

Tab skips a card or button

The element is a div or span with onClick, not a button or link.

Replace it with a button of type button, or a link for navigation.

You cannot see where focus is

outline none was written with no replacement.

Delete the rule and use one global :focus-visible rule with a thick outline.

The skip link exists, but Enter does not move focus

The target id does not exist, or is misspelled.

Make sure main has exactly the same id as the href, then try Tab and Enter.

A screen reader says only edit text or input

The input has only a placeholder, or for and id do not match.

Add a label whose htmlFor equals the input id.

Faint text is hard to read on a bright screen or outdoors

Contrast was judged by eye on a translucent background.

Measure the rendered colour, aiming for 4.5:1 for body text and 3:1 for large text.

An error appears when onClick is added to a page

The component using onClick is a Server Component.

Move the button into a small file with use client on the first line.

All animation vanishes, information included, when reduce is on

The panel was hidden instead of swapped for an instant change.

Shorten the transition or replace it with a fade, and keep the text visible.

Checklist before you publish

One h1 per page, headings in order, and one main in the layout.

Every clickable element is a real button or link.

Every input has a connected label, every informative image has alt text.

The skip link works and a focus ring is visible on every control.

Text contrast is measured: at least 4.5:1, or 3:1 for large text.

Tested with Tab, Lighthouse, axe-core and reduce motion switched on.

Go deeper

The free lesson explains the WCAG criteria behind these steps, with real code excerpts from this site, a quiz and one interactive explainer.

Web Accessibility Basics for Beginners in a Next.js Site

A fast page and an accessible page complement each other. Read

Next.js performance optimization

and

how to improve Core Web Vitals in Next.js

 for the speed side.

The rule to carry away: choose native elements first, keep focus visible, measure contrast, and ask for the motion preference before animating. None of it needs an extra library.

## Sources

- Next.js docs: Accessibility
- MDN: the label element
- W3C WAI: Understanding Success Criterion 1.4.3, Contrast (Minimum)
- MDN: prefers-reduced-motion
- GitHub: dequelabs/axe-core
- GitHub: GoogleChrome/lighthouse

## Source links

- https://nextjs.org/docs/architecture/accessibility
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/label
- https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
- https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion
- https://github.com/dequelabs/axe-core
- https://github.com/GoogleChrome/lighthouse

## FAQ

### How do I make a Next.js website accessible?

Start with semantic HTML: one main, one h1 per page, real buttons and links, and a label on every input. Then add a skip link, keep a visible :focus-visible ring, measure text contrast at 4.5:1 or better, and honour prefers-reduced-motion. Finish by tabbing through the page yourself.

### Does Next.js make my site accessible automatically?

Only partly. Next.js includes a route announcer that reads the page title, the h1 or the pathname after client-side navigation, and it ships eslint-plugin-jsx-a11y. It cannot fix a div used as a button or low-contrast colours, so you still have to build and test those yourself.

### Why does Tab skip my clickable card?

The card is probably a div or span with an onClick handler. Those elements are not in the Tab order and do not respond to Enter or Space. Use a real button for actions or a link for navigation, and keep the styling in CSS.

### Can Lighthouse or axe-core prove my site is accessible?

No. The axe-core README says it finds on average 57 percent of WCAG issues automatically, and Lighthouse reports only its automated audits in the score. Use them as a first pass, then test with the keyboard and a screen reader.

### What contrast ratio does my text need?

WCAG 2.2 criterion 1.4.3 at level AA asks for at least 4.5:1 for text and at least 3:1 for large-scale text, meaning 18 point or 14 point bold. The ratios are thresholds and must not be rounded. Measure the final rendered colours, especially over translucent backgrounds.

---

- HTML version: https://www.matthewswong.com/en/blog/cara-membuat-website-accessible-nextjs/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/cara-membuat-website-accessible-nextjs/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
