Blog

What is accessibility?

When we think about accessibility on the web, we often imagine something technical, reserved for developers or designers. But in reality it concerns anyone who publishes content online: from founders who set priorities, to designers, all the way to those who write a simple blog post.

An accessible web means a web that anyone can use, regardless of their abilities, the device they use, or the context they are in (slow connection, sunlight on the screen, noise, etc.). Improving accessibility not only helps people with disabilities, but makes the experience clearer, more predictable, and more pleasant for everyone.

Why accessibility matters

Digital accessibility removes barriers that prevent millions of people from accessing online content, tools, and services. Think, for example, of:

  • people with visual impairments, who use screen readers to “read” the page;
  • people with motor disabilities, who cannot use a mouse and rely on the keyboard;
  • people with hearing impairments, who need captions and transcripts;
  • people with cognitive disabilities, who struggle with long texts, complex interfaces, or unclear error messages.

A well‑designed site should, for example, allow keyboard navigation for those who cannot use a mouse. Or think of people with color blindness: if an important button is only red, without an icon or clear label, it may be hard to spot.

But the truth is that accessibility also helps those who do not have a permanent disability:

  • someone watching a video at the office without sound → uses captions;
  • someone reading on a smartphone in full sunlight → needs high contrast;
  • someone with one hand busy → appreciates large, easily clickable buttons.

In short: it is not just about “helping someone”, but about creating a digital environment that works better for everyone.

The importance of universal design

A key aspect of accessibility is the concept of Universal Design, meaning the design of products and environments so that they can be used by as many people as possible, without the need for special adaptations.

When we develop an application or a website, we should think from the beginning about who will use it and ensure that the experience is smooth for everyone. This means taking care of:

  • readability (typography, contrast, spacing);
  • predictability (consistent patterns, same components for the same actions);
  • tolerance for error (forms that explain what went wrong and how to fix it).

An interface with well‑spaced buttons not only helps those with motor difficulties, but also improves usability for those browsing on mobile or in low‑visibility conditions. Likewise, clear and consistent labels in forms help everyone, not just people with cognitive difficulties.

Example: a well‑designed button

<button class="btn-primary">
	<span aria-hidden="true">✅</span>
	<span>Confirm order</span>
</button>
.btn-primary {
	background-color: #0053b3; /* dark blue */
	color: #ffffff; /* white */
	padding: 0.75rem 1.25rem;
	border-radius: 0.375rem;
	border: none;
	font-size: 1rem;
	cursor: pointer;
}

.btn-primary:hover {
	background-color: #003f86; /* slightly darker blue */
}

.btn-primary:focus-visible {
	outline: 3px solid #ffcc00; /* yellow */
	outline-offset: 3px;
}
  • Contrast: dark blue on white has a high contrast.
  • Focus: those using the keyboard can clearly see where the focus is thanks to the yellow outline.
  • Icon + text: the icon is decorative (aria-hidden="true"), the text is what the screen reader actually reads.

The fundamental principles of accessibility

Web accessibility is based on four key principles, known as the POUR principles:

Perceivable

Content must be presented in ways that everyone can perceive, through at least one sensory channel (sight, hearing, touch).

Example: image with alternative text

<img
	src="/images/dashboard.png"
	alt="Line chart showing a constant increase in sales over the last 6 months"
/>

If the image is purely decorative:

<img src="/images/divider.svg" alt="" role="presentation" />

The “perceivable” principle is not only about images: it applies to all the content on the site, including video, audio, and the way we use colors.

Audio and video content

Videos must be accompanied by synchronized captions for those who cannot hear the audio. For audio‑only content (podcasts, interviews), a text transcript is essential. The <track> HTML element lets you associate subtitle files in WebVTT format directly with the native player:

<video controls>
	<source src="/videos/tutorial.mp4" type="video/mp4" />
	<track
		kind="subtitles"
		src="/captions/tutorial.it.vtt"
		srclang="it"
		label="Italiano"
		default
	/>
	<track
		kind="subtitles"
		src="/captions/tutorial.en.vtt"
		srclang="en"
		label="English"
	/>
</video>

Color must not be the only cue

A common mistake is using only color to communicate a state: for example, coloring an invalid field red without adding text or an icon. People with color blindness — about 4.5% of the global population — may not perceive the difference.

Only color

A screen reader does not read anything. A colorblind user does not distinguish the state.

Color + icon + text

Error Success Warning

A screen reader reads the state. It works for everyone.

<!-- ❌ Not accessible: only color distinguishes the states -->
<span class="dot dot-error"></span>

<!-- ✅ Accessible: color + icon + text + ARIA role -->
<span class="badge badge-error" role="status">
	<span aria-hidden="true">✗</span>
	<span>Error</span>
</span>

The same rule applies to charts: a pie chart that distinguishes slices only by color must also have text labels or a descriptive legend.

Operable

Everyone must be able to navigate the site, even without a mouse. This means interactive components must be reachable and activatable via keyboard, with a visible focus state.

Example: accessible main navigation

<nav aria-label="Main navigation">
	<ul>
		<li><a href="/about">About us</a></li>
		<li><a href="/services">Services</a></li>
		<li><a href="/blog">Blog</a></li>
		<li><a href="/contact">Contact</a></li>
	</ul>
</nav>

By using real <a> links with href, the navigation is automatically keyboard‑accessible (Tab) and can be activated with Enter/Space.

Keep focus always visible

One of the most common mistakes is removing the focus outline — often for aesthetic reasons — without replacing it with a visible alternative. For keyboard users, it is essential to know where focus is at any given time.

/* ❌ Never do this: it makes the focus state invisible */
:focus {
	outline: none;
}

/* ✅ Replace it with something clearly visible */
:focus-visible {
	outline: 3px solid #ffcc00;
	outline-offset: 3px;
}

The :focus-visible pseudo‑class is preferable to :focus because it is triggered only when focus is reached via keyboard or similar navigation — not by mouse click — thus avoiding the outline in contexts where it is not needed.

Respect motion preferences

Some users, especially those with photosensitive epilepsy or vestibular disorders, may find animations disturbing or even dangerous. Operating systems allow them to enable a preference called “Reduce Motion”, which browsers expose via a CSS media query.

Loading...

Try it yourself: activate "Reduce motion" in the accessibility settings of your operating system (macOS: Accessibility → Screen; Windows: Ease of Access → Display) and reload the page. The animation will automatically stop.

.spinner {
	animation: spin 0.8s linear infinite;
}

/* Disable the animation if the user has requested it */
@media (prefers-reduced-motion: reduce) {
	.spinner {
		animation: none;
	}
}

An even safer strategy is to disable animations by default and enable them only when the preference is not set:

/* Animation disabled by default */
.spinner {
	/* no animation */
}

/* Only activated if the user has no reduced‑motion preference */
@media (prefers-reduced-motion: no-preference) {
	.spinner {
		animation: spin 0.8s linear infinite;
	}
}

This media query applies to everything: transitions, parallax effects, automatic sliders, and any other motion.

Sufficient time

If your site has automatic carousels, temporary popups, or session timeouts, users must be able to pause, stop, or extend these mechanisms. A carousel that advances every three seconds without a pause button is a real barrier for screen reader users or people with cognitive difficulties — content disappears before they can process it.

Understandable

Content must be clear and predictable. Interfaces that change unexpectedly, cryptic error messages, or buttons with ambiguous labels make the experience difficult for everyone, not just for people with cognitive disabilities.

Example: form with clear labels and instructions

Fields marked with * are required.

<form aria-describedby="signup-help">
	<p id="signup-help">Fields marked with * are required.</p>

	<label for="email">Email *</label>
	<input id="email" name="email" type="email" required />

	<label for="password">Password *</label>
	<input id="password" name="password" type="password" required minlength="8" />

	<button type="submit">Create account</button>
</form>

The language of the page

Declaring the document language with the lang attribute on the <html> element is essential: screen readers use it to select the correct synthetic voice, with proper pronunciation and intonation. Without it, a synthesizer might read text using the wrong phonetic rules.

<!-- Page in Italian -->
<html lang="it"></html>

If a word or phrase is in a different language from the rest of the document, it should also be indicated locally:

<p>
	We adopt the
	<span lang="en">test-driven development</span>
	in all our projects.
</p>

Without lang="en" on the <span>, a screen reader set to Italian could pronounce “test-driven development” using Italian phonetic rules, making it hard to understand.

Predictability and consistency

A predictable interface reduces cognitive load. Concretely, this means:

  • the same components for the same actions across the site (the confirmation button always has the same label and position);
  • no unexpected context changes: the page does not submit automatically when the user selects an option from a dropdown, without an explicit confirm;
  • consistent labels: if an action is called “Save” on one screen, it should not become “Confirm” on another for no reason.

A classic counter‑example is a <select> with an onchange handler that automatically navigates to the selected page — without a separate “Go” button. Keyboard users may find themselves thrown onto a new page before they have finished choosing.

Robust

The site must be compatible with different assistive technologies, such as screen readers and voice commands, and work across various devices and browsers. This requires:

  • correct use of HTML semantics (<button>, <nav>, <main>, <header>, etc.);
  • a consistent heading structure (<h1><h2><h3>…);
  • using ARIA attributes only when necessary.

Semantics before ARIA

The first rule of ARIA (Accessible Rich Internet Applications) is: do not use it when semantic HTML is already enough. A native element is always more robust than a <div> with ARIA roles added, because the browser and assistive technologies support it natively — including keyboard handling, focus, and announcements.

<!-- ❌ div with ARIA: fragile, requires manual keyboard and focus handling -->
<div role="button" tabindex="0" onclick="..." onkeydown="...">Submit</div>

<!-- ✅ native button: accessible by default, no extra code needed -->
<button type="submit">Submit</button>

ARIA instead becomes essential for building complex widgets that do not have a native HTML equivalent, such as tab panels, dialogs, comboboxes, and tree navigation:

<div role="tablist" aria-label="Profile sections">
	<button
		role="tab"
		aria-selected="true"
		aria-controls="panel-info"
		id="tab-info"
	>
		Information
	</button>
	<button
		role="tab"
		aria-selected="false"
		aria-controls="panel-security"
		id="tab-security"
	>
		Security
	</button>
</div>

<div role="tabpanel" id="panel-info" aria-labelledby="tab-info">
	<!-- Panel content -->
</div>

Here, role="tab" with aria-selected and aria-controls is necessary because HTML does not offer a native tab element. In these cases ARIA is the right tool.

Consistent heading structure

Screen readers allow users to navigate headings as if they were a table of contents — it is one of the most common ways to move around a page. An incorrect hierarchy makes this navigation confusing.

Each page should have only one <h1>, and subsequent levels should follow an increasing order without skipping levels:

<!-- ❌ Incorrect hierarchy: jumps from h2 to h4 -->
<h1>Products</h1>
<h2>Software</h2>
<h4>Cloud</h4>

<!-- ✅ Correct hierarchy -->
<h1>Products</h1>
<h2>Software</h2>
<h3>Cloud</h3>

Headings should not be chosen based on visual size, but on their semantic level in the content hierarchy. If you need large text that is not a heading, use a <p> with appropriate CSS styling.

Common problems and how to fix them

1. Text without sufficient contrast

A common problem is light gray text on a white background, which is hard to read for many people.

Not accessible example:

This is a text with a wrong contrast
.text-muted {
	color: #9e9e9e; /* light gray */
	background-color: #ffffff; /* white */
}

Improved version:

This is a text with a good contrast
.text-muted {
	color: #4a4a4a; /* darker gray */
	background-color: #ffffff;
}

To check contrast you can use tools like WebAIM Contrast Checker or Accessible Brand Colors. WCAG recommends a ratio of at least 4.5:1 for normal text and 3:1 for large text.

Here is an example from the WebAIM site to check the contrast of the two previous examples:

The example that fails the contrast ratio. WebAIM Contrast Checker 1

The example that passes the contrast ratio. WebAIM Contrast Checker 2

2. Missing text alternatives

Without alternative text, screen reader users have no way to understand what an image represents.

Incorrect example:

<img src="/avatars/user123.png" />

Correct version:

<img src="/avatars/user123.png" alt="Laura Rossi's avatar" />

If the image is purely decorative:

<img src="/decorations/line.svg" alt="" role="presentation" />

3. Complex navigation

An unclear structure makes it hard to understand where you are and how to reach the main sections. A very useful pattern is the skip link, which allows users to jump directly to the main content.

<a class="skip-link" href="#main-content">Skip to main content</a>

<header>...</header>

<main id="main-content">
	<!-- Main content -->
</main>
.skip-link {
	position: absolute;
	left: -9999px;
	top: 0;
	background: #000;
	color: #fff;
	padding: 0.5rem 1rem;
	z-index: 100;
}

.skip-link:focus {
	left: 0.5rem;
	top: 0.5rem;
}

4. Interactive elements too small

Buttons and links should be big enough to be easily activated, even by those with motor disabilities or using a finger on a touch screen. The minimum recommended size is approximately 44x44 pixels.

Example: extended clickable target

<a href="/profile" class="profile-link">
	<span class="label">Profile</span>
</a>
.profile-link {
	background-color: #0053b3;
	color: #ffffff;
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.5rem 0.75rem; /* increase clickable area */
}

In this way the clickable area is not only the text, but the whole block.

Accessible error handling in forms

As mentioned earlier, forms are often one of the most critical parts of a website. Ambiguous errors, generic messages, or positioned far from the field make it difficult to understand what to do.

An accessible form should:

  • link each input to its label;
  • clearly indicate which fields are required;
  • explain the error in a specific way and how to correct it;
  • communicate errors to assistive technologies.

Example of form with error handling

All fields are required unless otherwise indicated.

<form id="a11y-form" aria-describedby="form-help form-errors" novalidate>
	<p id="form-help">All fields are required unless otherwise indicated.</p>

	<div
		id="form-errors"
		class="visually-hidden"
		aria-live="polite"
		tabindex="-1"
	></div>
	<div
		id="form-success"
		class="visually-hidden"
		aria-live="polite"
		tabindex="-1"
	></div>

	<div class="field">
		<label for="email">Email</label>
		<input id="email" name="email" type="email" required aria-invalid="false" />
	</div>

	<div class="field">
		<label for="age">Age</label>
		<input
			id="age"
			name="age"
			type="number"
			min="18"
			required
			aria-invalid="false"
		/>
	</div>

	<button type="submit">Submit</button>
</form>
.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.form-container input.field-error-input {
	border-color: #b00020; /* dark red with good contrast */
	outline-color: #b00020; /* dark red with good contrast */
}

#form-errors {
	color: #b00020; /* dark red with good contrast */
	font-size: 0.875rem;
	margin-top: 0.25rem;
	margin-bottom: 0.5rem;
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
}

#form-success {
	color: #008600; /* green with good contrast */
	font-size: 0.875rem;
	margin-top: 0.25rem;
}

Simple error handling in JavaScript:

const form = document.querySelector('#a11y-form')

if (form instanceof HTMLFormElement) {
	form.addEventListener('submit', handleA11yFormSubmit)
}

function addFieldError(input, message) {
	if (!(input instanceof HTMLInputElement)) return
	input.setAttribute(
		'aria-invalid',
		'true',
	) /* signals to the screen reader that the field is in error */
	input.classList.add('field-error-input')
	const errorSpan = document.createElement('span')
	errorSpan.className = 'field-error'
	errorSpan.textContent = message
	if (input.parentElement) {
		input.parentElement.appendChild(errorSpan)
	}
}

function handleA11yFormSubmit(event) {
	event.preventDefault()

	if (!(form instanceof HTMLFormElement)) return false

	const errorsContainer = form.querySelector('#form-errors')
	const successContainer = form.querySelector('#form-success')
	if (!(errorsContainer instanceof HTMLElement)) return false

	errorsContainer.innerHTML = ''
	successContainer.innerHTML = ''
	const email = form.querySelector('#email')
	const age = form.querySelector('#age')

	let hasError = false
	const messages = []

	;[email, age].forEach((input) => {
		if (!(input instanceof HTMLInputElement)) return
		input.setAttribute('aria-invalid', 'false')
		input.classList.remove('field-error-input')
		const errorSpan = input.parentElement
			? input.parentElement.querySelector('.field-error')
			: null
		if (errorSpan) errorSpan.remove()
	})

	if (
		!(email instanceof HTMLInputElement) ||
		!email.value ||
		!email.validity.valid
	) {
		hasError = true
		messages.push('<span>- Insert a valid email address.</span>')
		addFieldError(email, 'Insert a valid email address.')
	}

	if (
		!(age instanceof HTMLInputElement) ||
		!age.value ||
		Number(age.value) < 18
	) {
		hasError = true
		messages.push('<span>- You must be at least 18 years old.</span>')
		addFieldError(age, 'You must be at least 18 years old.')
	}

	if (hasError) {
		errorsContainer.classList.remove('visually-hidden')
		errorsContainer.innerHTML = `
      <b>Some errors have been detected:</b>
      ${messages.join('')}
    `
		errorsContainer.focus()
	} else {
		// Demo: no real submit, only accessible success message
		successContainer.classList.remove('visually-hidden')
		successContainer.innerHTML = `
      <span>Form submitted successfully (demo): in this example the data is not actually sent.</span>
    `
		successContainer.focus()
	}

	return false
}

Key points:

  • aria-invalid="true" signals to the screen reader that the field is in error;
  • a summary of errors at the top of the form (with aria-live="polite") informs immediately those who cannot see the visual layout;
  • the messages explain what is wrong and how to correct it.

How to start making your site more accessible

You do not need a revolution to improve accessibility. Where to start depends on your situation: are you working on an existing site, or do you have the chance to build something from scratch?

You already have a site: audit and priorities

When accessibility was not considered from the beginning, the most effective way to improve it is to work by priority — fixing the most serious and most frequent barriers first, without trying to solve everything at once.

1. Run an automatic audit

Tools like Lighthouse (built into Chrome DevTools), WAVE (also available as a Chrome extension), or axe DevTools can identify the most obvious issues in a few seconds: insufficient contrast, input elements without label, images without alt, buttons without accessible text.

One important warning: no automated tool can catch all problems. Automation is estimated to cover about 30–40% of real issues. Manual testing is indispensable.

2. Test keyboard navigation

This test takes five minutes and often reveals serious barriers that no automated tool will find:

  • disable the mouse and open the page from scratch;
  • use Tab to move through interactive elements, Shift+Tab to go backwards;
  • use Enter and Space to activate buttons and links;
  • check that focus is always visible and that the tab order follows the logical structure of the page;
  • make sure you can exit every interactive component (menus, modals, carousels) without getting “trapped”.

3. Check contrast, alt text, and forms

  • Check the colors of main text, buttons, and links with tools like WebAIM Contrast Checker (minimum 4.5:1 for body text, 3:1 for large text).
  • Add text alternatives to meaningful images, icons, and charts; leave alt="" only for purely decorative images.
  • Review forms: every input must have an associated label, and errors must be descriptive and linked to the field via aria-describedby.
  • Reduce excessive animations and respect the prefers-reduced-motion media query.

4. Test with a screen reader

Screen reader testing is the most direct way to understand what someone who cannot see the page experiences. You do not need to be an expert:

  • macOS / iOS: VoiceOver is built into the system. Turn it on with Cmd + F5 on Mac, or via Settings → Accessibility on iPhone.
  • Windows: NVDA is free and widely used. JAWS is the most widely used professional screen reader.

Try navigating the page using only the screen reader, without looking at the screen. If you can understand the structure, find the main content, and complete key actions (fill out a form, activate a button), you are on the right track.

5. Fix issues by priority

Not everything can be solved in a single sprint. Use this order of priority as a guide:

  1. Complete barriers (users cannot complete an action): forms that cannot be submitted, modals without a focus trap, content that is inaccessible from the keyboard.
  2. Understanding problems (users struggle a lot): low contrast, images without alt text on informative content, errors with no message.
  3. Improvements (the experience can be smoother): focus order that could be improved, more descriptive alternative texts, missing landmarks.

To help you, you can use the WCAG 2.1 Quick Reference as a detailed checklist.

Starting from scratch: accessibility as a foundation

When you are building something new, accessibility is much cheaper. It is far easier to design things well from the beginning than to fix them later. Every compromise you make today in design or development becomes technical debt you will have to pay back in the future.

💅 Start from design

Even before writing code, some design decisions directly impact accessibility:

  • 🎨 choose a color palette with sufficient contrast from the start (tools like Accessible Brand Colors help you build palettes that are already compliant);
  • 📏 define a typographic scale in rem, never in fixed px (users who have set a larger font size in the browser will see it respected);
  • 📱 design touch targets of at least 44×44px for every interactive element;
  • 🔍 always plan a visible focus state in component designs — do not leave it as “to be defined later”.

🧠 Think in components, not individual pages

An accessible component system — buttons, inputs, modals, menus — ensures that every new page built with those components inherits accessibility with no extra effort. Building a good <Button> once means you never have to fix it again.

🤖 Automate wherever you can

  • 🔌 Integrate eslint-plugin-jsx-a11y into your React/JSX project: it statically flags common issues such as <img> without alt or <button> without text.
  • 📦 Add Lighthouse CI to your build pipeline: an accessibility score below a given threshold blocks the deploy before the issue reaches production.
  • 🧪 Write tests with Testing Library: its APIs encourage you to find elements as a real user would (by role, by label) instead of by class or id.

Respect HTML: the wisest choice

💡 There is one principle that applies both if you are improving an existing site and if you are starting from scratch: using semantic HTML is always the most robust choice.

Browsers have already had decades of work put into making native elements accessible: a <button> is automatically keyboard‑accessible, can be activated with Enter and Space, is announced correctly by screen readers, and responds to focus without a single line of JavaScript. A <div> with role="button" requires you to manually re‑implement all of this — and something is often forgotten.

<!-- This works with keyboard, screen reader, touch, everything -->
<button type="submit">Submit request</button>

<!-- This requires tabindex, onkeydown, role, aria-label... and often something is missing -->
<div class="btn" onclick="submit()">Submit request</div>

The same is true for page structure: <nav>, <main>, <header>, <footer>, <article>, <aside> are not just stylistic conventions. They are landmarks that screen readers use to build a map of the page, allowing users to jump straight to the section they are looking for.

A site built on correct semantic HTML — without <div> instead of <button>, without headings chosen for visual size instead of level, without tables used for layout — is accessible almost by definition and requires very little additional ARIA.

🚧 This is not a constraint: it is the shortest path to an inclusive site.

The importance of an accessibility statement

One often overlooked aspect is the need to publicly declare the accessibility level of your site. This is done through an accessibility statement, which informs users about what has been done to make the site accessible and about any remaining limitations.

A good accessibility statement should include:

  • Goal: which standards the site aims to meet (for example: “Aim for WCAG 2.1 level AA compliance”).
  • Scope: which sections or features of the site are covered.
  • Supported technologies: tested browsers, operating systems, and assistive technologies.
  • Known limitations: parts of the site that are not yet fully accessible, with any remediation plans.
  • Contacts: an email address or form to report accessibility issues.

A brief example might be:

We are committed to making this site as accessible as possible, in accordance with the WCAG 2.1 level AA standards. If you encounter barriers or difficulties using the site, you can contact us at accessibility@yourdomain.com. Every report will be reviewed and, where possible, we will use your feedback to further improve the site.

Making this information easy to find (for example in the site footer) shows commitment and transparency towards users.

📌 To help you write your accessibility statement (in Italy), you can read directly what the AGID says.

Conclusion

Making the web accessible is a collective responsibility that benefits everyone. It is not a goal to reach once and for all, but a continuous process of listening, designing, and improving.

Even small changes — one more alternative text, a corrected contrast, a form that explains errors more clearly — can make a big difference in someone’s experience.

🚀 Let’s start today, project by project, building a truly inclusive internet.

Topic:
a11y , blog , accessibility , web , people , wcag , design