26. Aug 2025Insight

Web accessibility made simple: a practical checklist for developers

Every website should be designed to be accessible to everyone, from experienced users to people using assistive technologies. To ensure this consistently and without compromise, we need clearly defined rules.

The minimum accessibility standard serves as an official basis for validating design proposals, a checklist for code review, and a basis for testing scenarios. Thanks to it, we can create websites that are accessible and comfortable for all users.

📚 You can find very well-written material on all the rules of the WCAG specification on the website: WCAG in Plain English​.

1. Semantic HTML and why not to use  <div> everywhere

HTML contains a number of tags that semantically describe the content on a website. If we use it correctly, e.g. we use <nav> for navigation, fill in <label> for each input, or write interactive elements as <button>, screen readers will better understand the content and structure of the website.

 

Tip: Generate semantic HTML using AI (I recommend Gemini PRO)

“Generate accessible HTML without styles from this screenshot”

Screenshot pre generovanie kódu.png

Gemini generated:

<section aria-labelledby="benu-headline">

    <div>
        <img src="path-to-your-image.png" alt="Benu Pharmacy app displayed on a mobile phone screen.">
    </div>

    <div>
        <h2 id="benu-headline">
            Creating a state-of-the-art experience for Benu pharmacies
        </h2>

        <dl>
            <div>
                <dt>10K+</dt>
                <dd>Average daily users</dd>
            </div>
            <div>
                <dt>99.9%</dt>
                <dd>Cash free rate</dd>
            </div>
            <div>
                <dt>4.8*</dt>
                <dd>App rating on Google Play Store &amp; App Store</dd>
            </div>
        </dl>

        <a href="/case-study/benu-pharmacies" aria-label="Read more about the Benu pharmacies case study">
            Read more
        </a>
    </div>

</section>

Summary of accessibility features (WCAG) in code:

  • Semantic structure: Using the <section>, <h2>, <dl> dtags gives the content a logical structure.
  • Text alternative: The alt attribute of the <img> tag ensures that the information in the image is also accessible to blind people.
  • Section labeling: Using aria-labelledby helps users of assistive technologies quickly orient themselves to the content of the page.
  • Clear links: aria-label for links <a> provides a clear description of the link destination, which is crucial for links with generic text such as "Read more".
  • Semantic accuracy: While <ul> (unordered list) is good,  <dl> (definition list) is even more accurate. It directly communicates the relationship between the value of the metric (<dt>) and its description (<dd>), which assistive technologies can interpret and better present to the user.

ℹ️ In addition, semantic HTML helps search engines better understand the content of a page, which also has a significant impact on search engine rankings.

Why it is important to use the correct HTML tags can be demonstrated using the button:

▶️ CodeSandbox example ◀️

<!-- Bad example ❌-->
<div className="button" onClick={handleClick}>
  Open something
</div>

<!-- Good example ✅ (onClick can only be used on focusable elements) -->
<button className="button" onClick={handleClick}>
  Open something
</button>

At first glance, the button works. When I click the mouse handleClick  function is called for both div and button. However, if I navigate the website using the keyboard (Tab and Enter combination), div becomes unusable. Browser automatically added the necessary functionality to the native <button> tag, e.g.. tabindex or focus state.

📚 A very well-written article about all HTML tags is available on MDN Web Docs.

2. ARIA (if there is no HTML alternative)

ARIA (Accessible Rich Internet Applications) is a set of special attributes that you can add to HTML code. These attributes are used exclusively by assistive technologies and do not affect the rendered content on the web in any way, but only provide additional information for assistive technologies.

⚠️ Use ARIA attributes wisely and only as a last resort if there is no HTML alternative! If you use ARIA attributes incorrectly, you can make your website even less accessible than if you did not use them at all.

Three main types of ARIA attributes:

1. Roles - Defines what element is.

  • It says to the reader: „This is not just an ordinary <div>, this is navigation or button.
  • Examples: role="navigation", role="dialog", role="button", role="search".
    • HTML tags, e.g. <nav> or <button>, automatically add a role, so you need to use the correct HTML tags! The best ARIA is no ARIA.
    • If you use the ARIA role for an interactive element, e.g. role="tab", role="slider", the role itself does not add any functionality. It only informs the screen reader. Your task is to program keyboard controls, e.g. arrows, Esc, Enter, using JavaScriptu.
❌ Bad example: <div role="button" tabindex="0" onclick="myFunkcia()">Save</div>
✅ Good example: <button onclick="myFunkcia()">Save</button>

2. Properties - Defines properties and relationships of element.

  • They describe characteristics that do not usually change.
  • Examples: aria-labelledby="id-description" (tells you where to find the text description), aria-required="true" (indicates a required field), aria-invalid="true" (indicates that the value is incorrect).

3. States - Defines current state of element.

  • They describe conditions that change based on user interaction (usually using JavaScript).
  • Examples: aria-expanded="false" (indicates that the drop-down element is collapsed), aria-hidden="true" (hides element from reader), aria-disabled="true" (indicates that element is inactive).

📚 You can find more information about the correct use of ARIA in the MDN documentation.

 

⚠️ Standard HTML tags usually do not need to implement ARIA attributes, but custom solutions require extra care. We usually use third-party libraries for non-standard functionalities such as dialog, swiper, datepicker, etc. Make sure that these libraries implement ARIA attributes correctly!

3. Important features of the site

1. Filled in page name <title>

Every HTML page must have a unique and descriptive <title> element in the <head> section.  Good title is unique, concise, and descriptive. Follow these recommendations:

  • Put the page in context: Always include the name of the specific subpage and the name of the entire website. The recommended format is: Specific page name |  Website name.
  • Be specific: Instead of "Services," use "Graphic Design | Creative Studio." Instead of "Profile," use "John Smith's Profile | Our Company".
  • Most important at the beginning: Since names are often shortened in tabs and search results, place the most important information (the name of the specific page) at the beginning.
  • Brevity: The ideal length is approximately 50-60 characters so that the entire name is displayed in search results.

2. Filled lang attribute in html tag

  • App must have a programmatically specified main content language (e.g., Slovak, lang="sk"). This allows assistive technologies, such as screen readers, to correctly interpret and pronounce the text.
  • When changing the language of the page (e.g., via a dropdown menu), lang must also be dynamically adjusted using JavaScript.

3. Part of the page in another language

  • If the text contains a word, phrase, or paragraph in a language other than the main language of the page, this section must also be marked using the lang attribute.
<!DOCTYPE html>
<html lang="en"> ✅ Filled lang attribute in html tag
<head>
    <meta charset="UTF-8">
    <title>Custom web design | Company Name</title> ✅ Filled title
    </head>
<body>
    <h1>Our web design services</h1>
    <p>We offer modern and accessible websites...</p>
    
    <blockquote lang="sk"> ✅ If text is in a different language than entire page
	    <p>Jediný spôsob, ako odvádzať skvelú prácu, je milovať to, čo robíte.</p>
	    <footer>- Steve Jobs</footer>
		</blockquote>
</body>
</html>

4. Navigation using the keyboard and focus

Not everyone uses a mouse to navigate on the web. Some cannot use a mouse due to illness, experienced users control the web using a keyboard, or we control the web on TV using a remote control... Such users rely on all interactive elements on the web, such as buttons, links, or forms, having their focus state.

You probably would not like it if the cursor did not show up on the website. However, we often do this for users who navigate the website using their keyboard.

body {
  cursor: none; /* you definitely wouldn't do this on the web */
}

:focus {
  outline: none; /*  so please don't do this either */
}

However, :focus has one feature that forces developers to delete the outline. When you click on a button, for example, a frame appears around it, which does not look good. In modern browsers, we can easily solve this problem by using :focus-visible instead of :focus. This selector is already supported by all modern browsers, and thanks to it, the frame is only displayed when navigating with the keyboard (ffocusing on the button using Tab key).

It is necessary to ensure that the focus state is sufficiently visually distinct:

  • Focus is at least 2px thick and must have a contrast ratio of 3:1 (level AAA).
Porovnanie dobrého a zlého indikátora zamerania pre tlačidlo „Learn More“; dobrý má hrúbku 4px a kontrastný pomer 8:1, zlý má hrúbku 1px a kontrastný pomer 2:1.

There must be a color contrast ratio of at least 3:1 (level AAA) between the focused and unfocused states.

  • Focus status for form fields should also change the thickness of the border. Changing the color should not be the only way to distinguish the status of an active field.
Dva kontaktné formuláre vedľa seba s poľami pre meno, e-mail a správu; oba obsahujú modré tlačidlo „Submit“ a líšia sa odtieňom pozadia.

Focus is at least 2px thick and must have a contrast ratio of 3:1 compared to the unfocused state (level AAA).

  • Keyboard focus is not completely covered by other content (level AA). Focus is not covered by content at all (level AAA).

 

nesprávny príklad.png

 

  • Usually, we want our own style for the focus state that better suits our design. In that case, we can override the browser's default style:
// Example of how we can add our own style for the focus state

button:focus-visible {
  outline: 3px solid deepskyblue;
  outline-offset: 3px;
}

 

focuv stav príklad.gif

Tabindex

Interactive elements, such as buttons, form fields, or links, are tabbable and do not require any additional implementation. If we want to achieve this for <div>, for example, we can assign it <div tabindex="0">. We can use this option, for example, in interactive graphs that do not have a representative tag in HTML. If we want to achieve the opposite effect, i.e. disable interactivity (we cannot select the element using Tab key) we use tabindex="-1". We should not use any other values fo tabindex because they change the order of elements, which is usually not what we want.

DOM determines order

Tab order is determined by the order of elements in HTML (DOM), not by the visual layout you create using CSS (e.g. flex-direction: column-reverse or order). Focus order when navigating with the keyboard must be logical and correspond to the visual layout of the page.

On this topic, I recommend reading the article: Indicating focus to improve accessibility.

5. Skip links

Purpose of skip links is to allow people to skip repetitive sections or blocks of content on web pages and make it easier for them to access the main content of the page. The most common example is skipping the main navigation. For people who use keyboard navigation, it is frustrating to have to tab through all the links on every page.

 

skip to content.png

 

How to properly implement a skip to content link:

<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
    <a href="#main-content" class="skip-link">Skip to main content</a>

    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/about-us">About us</a>
            <a href="/contact">Contact</a>
        </nav>
    </header>

    <main id="main-content">
        <h1>Welcome to our website</h1>
        <p>This is the main content that the user accesses after clicking on the link.</p>
        <a href="/example">Next link in content</a>
    </main>
</body>
</html>
.skip-link {
    /* Visually hiding a link outside the screen */
    position: absolute;
    left: -999px;
    top: -999px;
    
    /* Style according to the design of the page, e.g... */
    padding: 20px;
    backround-color: #FFF
    color: primary;
    font-size: 1.2em;
}

.skip-link:focus {
    /* Highlight the link when it receives focus (e.g., with Tab key) */
    position: absolute; /* Or 'fixed', as needed */
    top: 0px;
    left: 0px;
}

ℹ️ Get inspired by websites such as smashingmagazine. After loading the page, press Tab key. Skip to main content will appear in the upper left corner.

 

⚠️ Do not use Next.js <Link> component, but use the classic native HTML <a> tag. Link component incorrectly implements the focus state change. More info in the GitHub issue.

6. Empty links and buttons

Every interactive element on the page, such as buttons or links, must have a clear name. It often happens that links only contain an icon and no text. This means that screen readers cannot tell blind users what the link is.

 

Prázdne tlačidlá.png

 

Using the <span> tag, which we hide with CSS but remains visible to screen readers, we can display descriptions for buttons using only svg icons.

// CSS
.sr-only {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
<!-- HTML -->
<a href="https://twitter.com">
  <svg aria-hidden="true" ...></svg>
  <span class="sr-only">Twitter</span>
</a>

We used the aria-hidden="true" attribute for svg element. Icons on the website serve primarily as a visual aid, but they are meaningless to blind people and unnecessarily clutter more important content, such as text in buttons.

📚 You can find more information about methods for hiding visual content on the WebAIM website.

Inappropriate link/button descriptions

  • For buttons or links, it is important to describe what will happen after clicking. A common example:

nesprávny popis buttonu-en.png

// Bad examples ❌
<button>Facebook</button>
<button>LinkedIn</button>

// Good examples ✅
<button>Share this article on Facebooku</button>
<button>Sshare this article on LinkedIn</button>
  • Another common problem is links that have no informative value. For example, click here, read more, etc. Screen readers can read a list of all links on a page to the user so that they can navigate more quickly. However, if the reader says "read more" five times in a row, the user has no idea where the link leads.
// Bad examples ❌
<a href="https://www.goodrequest.com/blog">Click here</a>
<a href="https://www.goodrequest.com/blog">Learn more</a>
<button>Edit</button>

Ideal solution is to provide meaningful text for this link/button. If this cannot be avoided, we can use a hidden <span> element:

<a href="/blog">
	Show more
	<span class="sr-only">blog articles</span>
</a>

<a href="/blog/{article.url}">
	Read more
	<span class="sr-only">about {article.name}</span>
</a>

<!-- Often buttons in tables -->
<button onClick={handleChangeAddress(user.id)}>
	Edit
	<span class="sr-only">address for user {user.name}</span>
</a>

If you need to completely rewrite the content of the button for a screen reader, you can use aria-label attribute, but make sure that the text in aria-label matches the visible text.

✅ GOOD: 
<a href="url" aria-label="Read more about accessibility">Read more</a>
<button aria-label="Send application">Send</button>

❌ BAD (text does not match): <button aria-label="Submit application">Send</button>

📚 You can find more information about the correct description of links in the WCAG specification

7. Form accessibility

Control via keyboard

Forms on websites, like other interactive elements, should be accessible using a keyboard. For example, we can move between inputs using Tab key, select should be selectable using the up/down arrows, or checkboxes should be selectable using the space bar.

Naming inputs

Each input must have a visible <label> element assigned to it. We can use two notations using the for attribute in combination with id or wrapping the input in a <label> element.

<label for="name">Name:</label>
<input id="name" type="text" autocomplete="name">

<!-- or -->

<label>Name: <input type="text" autocomplete="name"></label>

Grouping

Checkboxes or radio inputs can be grouped using <fieldset> element and assigned a description using <legend> element. However, visually grouping inputs is not sufficient for people who use screen readers.

<fieldset>
  <legend>Select your pizza toppings:</legend>
  <input id="ham" type="checkbox" name="toppings" value="ham">
  <label for="ham">Ham</label><br>
  <input id="mushrooms" type="checkbox" name="toppings" value="mushrooms">
  <label for="mushrooms">Mushrooms</label><br>
  <input id="olives" type="checkbox" name="toppings" value="olives">
  <label for="olives">Olives</label>
</fieldset>

Error handling and validation

  • If we want to mark an input as mandatory, we can add the attribute required or aria-required="true". Marking it with an asterisk is not enough, because not all screen readers recognize that it is a mandatory input.
<input id="name" type="text" required>

<!-- or -->

<input id="name" type="text" aria-required="true">
  • For visually impaired users, it is not enough to highlight invalid input visually, e.g. with red color. aria-invalid="true" attribute is used for this purpose, thanks to which the screen reader notifies the user of the error.
<input id="name" type="text" aria-invalid="true">
  • Link the error message to the field using aria-describedby. The value of aria-describedby is id of the element that contains the error message. Screen readers will then read the field label, its type, value, and then the error message.
<label for="userEmail">E-mail:</label>
<input aria-describedby="emailError" aria-invalid="true" id="userEmail" type="email" name="email">
<div id="emailError" class="error-message">
  Please enter a valid email address.
</div>
  • After submitting a form with errors, move the focus to the first error field or to the error summary. This helps keyboard and screen reader users quickly find problems.

📚 You can find well-written material on form accessibility at WebAIM.

8. Status reports and notifications

On the web, some actions do not cause the page to reload completely. User can:

  • Add a product to the shopping cart.
  • See search results appear on the page.
  • Submit a form that is saved without redirection.
  • Encounter an error after performing an action.

Sighted user perceives these changes visually. However, a screen reader user does not know that something has happened.

 

Oznámenie.png

 

How to technically implement screen reader notifications?

Option 1 is to set role attribute:

<!-- automatically adds aria-live="polite" and aria-atomic="true" -->
<div role="status"> {{announcementString}} </div>

<!-- automatically adds aria-live="assertive" and aria-atomic="true" -->
<div role="alert"> {{announcementString}} </div>

For regular (non-urgent) reports: role="status"

Reader reads the message when it is not announcing other content (it is idle). This is the option for 95% of cases.

  • “Item has been added to your shopping cart.”
  • “Your profile has been updated.”
  • “25 search results for 'notebooks' are displayed.”
  • "Message has been sent successfully."

 

príklad oznámenia.png

 

For urgent reports: role="alert"

For critical errors or time-sensitive warnings that require immediate user attention, warning is announced immediately and does not wait until the screen reader is idle.

Option 2 is to explicitly use aria-live a aria-atomic (recommended):

  • aria-live:
    • "polite": Notify change when nothing else is happening.
    • "assertive": Will immediately interrupt the user and notify them of the change.
  • aria-atomic:
    • "true": Reads the entire content of the element.
    • "false": Will only read the changed part.
<div aria-live="polite" and aria-atomic="true"></div> <!-- same as role="status" -->
<div aria-live="assertive" and aria-atomic="true"></div> <!-- same as role="alert" -->

Further examples of how to use aria-live and aria-atomic:

<!-- aria-atomic="false", because we only want to report new messages -->
<div id="chat-log" aria-live="polite" aria-atomic="false">
  <p>Jane Doe: Hi everyone!</p>
  <p>John Smith: Hello there!</p> 
</div>

<ul id="live-scores" aria-live="polite" aria-atomic="false">
  <li>Team A vs. Team B: 1-0</li>
  <li>Goal! Team A scores. The score is now 2-0.</li>
</ul>

<!-- aria-atomic="true", because we want to report the entire text and not just the changes -->
<div id="search-summary" aria-live="polite" aria-atomic="true">
  Showing 15 of 240 products.
</div>

<div id="stepper-status" aria-live="polite" aria-atomic="true">
  Step 2 of 5: Shipping Details
</div>

<div id="toast-notification" aria-live="polite" aria-atomic="true">
  File saved successfully.
</div>


<!-- aria-live="assertive", because we want to report a critical error -->
<div aria-live="assertive" aria-atomic="true">
  An unknown error occurred while saving your changes. Try again.
</div>

<div aria-live="assertive" aria-atomic="true">
  Connection to server lost. Your changes may not be saved.
</div>

Example of how to make accessible search:

function AccessibleSearch() {
  const [searchStatusMessage, setSearchStatusMessage] = useState('');

  const handleSearch = (searchTerm: string) => {
    // Search logic here
    setSearchStatusMessage(`${resultsCount} results found for ${searchTerm}`)
  };

  return (
    <>
      {/* 1. Use a form with role="search" for a landmark */}
      <form role="search" onSubmit={handleSearch}>
        {/* 2. Connect the label to the input */}
        <label htmlFor="search-input">Search our site</label>
        <input
          type="search"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {/* 3. ARIA live region to announce status updates */}
      <ScreenReaderOnlySC role="status">{searchStatusMessage}</ScreenReaderOnlySC>
    </>
  );
}

ℹ️ Mantine notifications implement ARIA Live Regions and correctly report the content of notifications to screen readers.

9. Clear error messages

Errors must be clearly marked and described to the user. If the user makes a mistake when filling out a web form, all errors they have made must be clearly identifiable. The error message should state exactly what went wrong and how to fix it.

 

error hlášky.png

 

Some examples of useful error messages:

  • If the input must match a set of allowed values, the error message should include the allowed options.
  • Describe what the correct data and formatting should look like.
  • Show similar correct values and explain how to enter them.

error hlásenie.png

Clear feedback after successful submission

Providing clear feedback after successful form submission gives users confirmation that they do not need to search through the form or page for errors. By displaying clear and consistent feedback, users can quickly understand that their action has been completed.

 

Príklad správneho hlásenia-en.png

10. Gestures

Any action that requires a complex gesture (swiping, pinch-to-zoom, dragging, shaking, tilting) must also be possible with a simple action, such as clicking a button or pressing a key. No function should rely solely on motion gestures. Motion control can be a supplement, but never the only way to do something.

Examples:

  • Carousel: In addition to swiping, add visible Next and Back arrows..
  • Slider: In addition to dragging the slider, add buttons to increment/decrement the value, or verify that the arrow keys on the keyboard work.
  • Map: In addition to pinch-to-zoom, add + and - buttons.
Dve mapové rozhrania: vľavo gesto ruky smerujúce na lokalizačný marker s otáznikmi; vpravo gesto ruky používajúce ovládacie prvky na priblíženie a navigáciu po mape.

Map on the left relies solely on gesture controls (pinch to zoom and swipe). The map on the right displays proper zoom buttons and navigation arrows, which help users understand how to control the content even without using pointing gestures.

  • Kanban boards: In the card details, we can change the status, e.g. using a dropdown menu and changing the status from "To Do" to "In Progress" or using a library that implements drag&drop using the keyboard.
  • Gestures "shake to undo": Use buttons or links to achieve the same functionality.

 

gestá-potrasením späť.png

11. Additional rules

  • Focus trap: There must be no situation where you open, for example, a modal window, cookie banner, video, etc. using the keyboard and cannot return to the page you came from. A close button or functional ESC key must always be available on the modal window. We cannot rely solely on clicking outside the modal window.
  • Keyboard shortcuts consisting of a single character (e.g., pressing the letter "M" to mute the sound): Website must provide a mechanism to disable the shortcut, remap it to a combination with a modifier key (e.g., Ctrl or Alt), or ensure that it is only active when a specific element on the page has focus. The goal is to prevent users from accidentally triggering assistive technologies when navigating the website using the keyboard.
  • Important forms, such as legal agreements or financial disclosures, offer the option to review the information entered before submitting: Display a summary page that summarizes all the data entered by the user and offers the option to edit the data or return to editing.
  • Display a confirmation message before deleting or modifying important data/files: Ideally, the user should be able to restore the file after deletion or undo the action.
Andrej NemečekHead of Frontend