Accessibility

Bootstrap · Lesson 26 of 28 8 min read Updated

Accessibility (often shortened to a11y) means people with disabilities can use your website: someone who is blind and uses a screen reader, someone who cannot use a mouse and moves with the keyboard, someone with low vision who zooms to 200%, someone with colour blindness. In India alone, millions of people live with a disability, and every one of us will at some point use a site with a broken arm, in bright sunlight or on a slow phone.

Bootstrap's components are designed to be accessible, but only if you write the HTML correctly. In this lesson you will learn what Bootstrap does for you, what it cannot do, and how to check your pages.

What Bootstrap does, and what you must do

Bootstrap gives youYou still must
Keyboard support in dropdowns, tabs, modals, offcanvas and carouselsUse real <button> and <a href> elements, not clickable <div>s
Focus kept inside open modals and offcanvas panelsGive each modal and panel a title linked with aria-labelledby
Automatic updates of aria-expanded, aria-selected and similar statesWrite the starting ARIA attributes correctly in your HTML
Visible focus rings on buttons, links and form fieldsNever remove focus outlines without a replacement
Reduced animation when the user asks for itCheck colour contrast and write meaningful text and alt attributes

The golden rule: use the right HTML element first. A <button> is focusable, works with Enter and Space, and is announced as "button" without any extra attributes. A <div class="btn"> needs a lot of extra code to do the same, and usually still misses something.

visually-hidden hides text from the screen but keeps it available to screen readers. Use it to add context that sighted users get from the layout:

HTML
<button type="button" class="btn btn-outline-danger btn-sm">
  Remove <span class="visually-hidden">Gulab jamun, 1 kg</span>
</button>

A sighted user sees "Remove" next to the item. A screen-reader user moving through a list of ten "Remove" buttons hears exactly which item each one removes.

visually-hidden-focusable hides an element until it receives keyboard focus. Its main use is a skip link: the first link on the page, which lets keyboard users jump past a long navbar straight to the content.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Accessible page structure</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <a class="visually-hidden-focusable position-absolute top-0 start-0 m-2 p-2 bg-body border rounded z-3" href="#main">Skip to main content</a>

  <header>
    <nav class="navbar navbar-expand bg-body-tertiary" aria-label="Main navigation">
      <div class="container">
        <a class="navbar-brand" href="#home">Rohtak Civic Library</a>
        <ul class="navbar-nav">
          <li class="nav-item"><a class="nav-link active" aria-current="page" href="#home">Home</a></li>
          <li class="nav-item"><a class="nav-link" href="#books">Books</a></li>
          <li class="nav-item"><a class="nav-link" href="#events">Events</a></li>
        </ul>
      </div>
    </nav>
  </header>

  <main id="main" class="container py-4" tabindex="-1">
    <h1 class="h3">Click inside this result, then press Tab</h1>
    <p>The first Tab press shows the hidden "Skip to main content" link at the top left.</p>

    <h2 class="h5">Your borrowed books</h2>
    <ul class="list-group mb-3" style="max-width: 440px">
      <li class="list-group-item d-flex justify-content-between align-items-center">
        Godan, Premchand
        <button type="button" class="btn btn-sm btn-outline-primary">Renew <span class="visually-hidden">Godan</span></button>
      </li>
      <li class="list-group-item d-flex justify-content-between align-items-center">
        Malgudi Days, R. K. Narayan
        <button type="button" class="btn btn-sm btn-outline-primary">Renew <span class="visually-hidden">Malgudi Days</span></button>
      </li>
    </ul>

    <a href="#rules" class="link-primary focus-ring py-1 px-2 rounded text-decoration-none">Library rules (custom focus ring)</a>
  </main>

  <footer class="container py-3 border-top small">Open 9 am to 7 pm, closed on Mondays.</footer>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Things to notice:

  • Landmarks: <header>, <nav aria-label>, <main> and <footer> let screen-reader users jump between page areas. Give every <nav> a different label when there is more than one.
  • The skip link points at #main. The <main> has tabindex="-1" so it can receive focus when the link is followed.
  • Headings form an outline: one <h1>, then <h2> for sections. The classes h3 and h5 only change the size.
  • focus-ring (new in 5.3) adds a Bootstrap-style focus ring to elements that do not have one, such as a plain link styled as a box. Change its colour with focus-ring-success and similar classes.

ARIA: states and names

ARIA (Accessible Rich Internet Applications) attributes tell assistive technology what a custom widget is and what state it is in. Bootstrap's examples include the right ones; keep them when you copy, and keep them correct when you change things:

AttributeUsed forExample
aria-labelA name when there is no visible textIcon-only buttons, the navbar toggler
aria-labelledbyPoints at the element that names this oneModals and offcanvas point at their title
aria-describedbyPoints at extra descriptionForm help text and error messages
aria-expandedWhether a collapsible thing is openDropdown toggles, accordion buttons
aria-controlsWhich element a button controlsCollapse and tab buttons
aria-current="page"The current item in a setActive nav links, breadcrumbs, pagination
aria-live, role="status", role="alert"Announce changes without moving focusToasts, alerts, "Saved" messages
aria-hidden="true"Hide decoration from screen readersIcons next to text

Bootstrap updates aria-expanded, aria-selected and aria-pressed for you when its JavaScript runs. When you change something with your own JavaScript, such as a custom toggle or a live search result count, you must update the ARIA attributes yourself.

Colour contrast

Text must stand out from its background. WCAG, the international accessibility standard, asks for a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (AA level). Some of Bootstrap's default colours are too light to use as text on white:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Contrast and live regions</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container py-4">
    <p class="text-warning">text-warning on white: about 1.6:1, fails</p>
    <p class="text-info">text-info on white: about 2:1, fails</p>
    <p class="text-warning-emphasis">text-warning-emphasis: dark enough, passes</p>
    <p class="p-2 text-bg-warning rounded">text-bg-warning: dark text on yellow, passes</p>

    <hr>
    <label for="qty" class="form-label">Copies to reserve</label>
    <div class="d-flex gap-2 align-items-center" style="max-width: 320px">
      <input type="number" id="qty" class="form-control" value="1" min="1" max="3">
      <button type="button" class="btn btn-primary" id="reserveBtn">Reserve</button>
    </div>
    <p role="status" class="mt-2" id="status"></p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    // role="status" is a polite live region: screen readers announce its new text automatically.
    document.getElementById("reserveBtn").addEventListener("click", () => {
      const qty = document.getElementById("qty").value;
      document.getElementById("status").textContent = `${qty} copy reserved. Collect it within 2 days.`;
    });
  </script>
</body>
</html>

Use text-warning and text-info only for large text or on dark backgrounds. For small text, use the -emphasis versions or text-bg-*. Browser developer tools show the contrast ratio when you inspect a text element, so you can check any colour pair.

Reduced motion

Some people feel dizzy or sick from animations. Operating systems have a "reduce motion" setting, which websites can read with the prefers-reduced-motion media query. Bootstrap respects it: when the setting is on, its fades, slides and collapse animations are switched off (controlled by the Sass setting $enable-reduced-motion, which is on by default). Do the same in your own CSS:

CSS
@media (prefers-reduced-motion: reduce) {
  .hero-banner { animation: none; }
}

Autoplaying carousels also move content, so give them a pause button or avoid autoplay; see Carousel.

Testing your page

You do not need special equipment to catch most problems:

  1. Keyboard only. Put the mouse away. Use Tab and Shift+Tab to move, Enter and Space to press, Escape to close, arrow keys in menus and tabs. Can you reach and use everything? Can you always see where the focus is?
  2. Zoom to 200%. Does the layout still work without sideways scrolling?
  3. Automatic checks. Run Lighthouse (built into Chrome's developer tools) or a browser extension such as axe DevTools. They catch missing labels, low contrast and missing alt text.
  4. A screen reader. NVDA is free on Windows, TalkBack is built into Android, and VoiceOver is built into Apple devices. Listen to one page from top to bottom.

Older code

Bootstrap 4 called the hidden-text classes sr-only and sr-only-focusable; Bootstrap 5 renamed them to visually-hidden and visually-hidden-focusable. If you upgrade a project and find text suddenly visible on screen, an old sr-only class is usually the reason. Bootstrap 3 also used sr-only, and its components had far fewer ARIA attributes, so old markup often needs ARIA added when you migrate.

Common mistakes

  • Clickable <div> or <span> elements. They cannot be reached with Tab. Use <button> or <a href>.
  • Removing focus outlines with outline: none and no replacement. Keyboard users get lost.
  • Copying Bootstrap examples without their ARIA attributes, or leaving aria-labelledby pointing at an id that no longer exists.
  • Colour as the only signal, such as a red border on an invalid field with no message.

Key points

  • Bootstrap's components are accessible only when your HTML uses the right elements and ARIA attributes.
  • visually-hidden adds screen-reader-only text; visually-hidden-focusable makes skip links.
  • Keep ARIA states (aria-expanded, aria-current, aria-live) correct, and update them yourself in custom JavaScript.
  • Check contrast: text-warning and text-info fail on white for small text; use -emphasis or text-bg-* classes.
  • Test with the keyboard, zoom, an automatic checker and a screen reader; aim for WCAG 2.2 AA.

Exercise

Take this broken snippet and fix every accessibility problem: <div class="btn btn-danger" onclick="del()"><i class="bi bi-trash"></i></div> and <p class="text-warning small">Your session expires in 2 minutes</p>. Then add a skip link to a page whose main content has the id content.

Show solution
HTML
<a class="visually-hidden-focusable" href="#content">Skip to main content</a>

<button type="button" class="btn btn-danger" id="deleteBtn" aria-label="Delete order">
  <i class="bi bi-trash" aria-hidden="true"></i>
</button>

<p class="text-warning-emphasis small" role="status">Your session expires in 2 minutes</p>

<main id="content" tabindex="-1">...</main>

<script>
  document.getElementById("deleteBtn").addEventListener("click", () => del());
</script>

The <div> becomes a real <button> with an accessible name, the icon is hidden from screen readers, the low-contrast text-warning becomes text-warning-emphasis, and role="status" lets screen readers announce the warning. The skip link is the first focusable element and targets the focusable <main>.

Quick Quiz

1. What does the visually-hidden class do?

2. What is a skip link?

3. Which Bootstrap 4 class was renamed to visually-hidden in Bootstrap 5?

4. Why should small text not use text-warning on a white background?

Learn Bootstrap with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call