Bootstrap 5 ships with better accessibility defaults than most frameworks, but "better defaults" doesn't mean "automatically accessible." Here's what you still need to handle yourself.

1. Use Real Semantic Elements

Bootstrap's classes work on any element, which tempts people to style divs instead of using proper interactive elements:

<!-- Bad: div styled as a button, not keyboard accessible -->
<div class="btn btn-primary" onclick="submitForm()">Submit</div>

<!-- Good: real button, keyboard accessible by default -->
<button type="submit" class="btn btn-primary">Submit</button>

<!-- Bad: div as a link -->
<div class="nav-link" onclick="navigate()">Home</div>

<!-- Good: real anchor -->
<a href="/" class="nav-link">Home</a>

Real <button> and <a> elements get keyboard focus, Enter/Space activation, and proper screen reader announcements automatically. Styled divs get none of that without manually adding tabindex, role, and keyboard event handlers.

2. Icon-Only Buttons Need aria-label

Visual icons mean nothing to screen readers without text:

<!-- Bad: screen reader announces nothing meaningful -->
<button class="btn btn-sm btn-outline-secondary">
  <i class="bi bi-x-lg"></i>
</button>

<!-- Good: aria-label provides the missing context -->
<button class="btn btn-sm btn-outline-secondary" aria-label="Close notification">
  <i class="bi bi-x-lg"></i>
</button>

<!-- Bootstrap's built-in close button already handles this -->
<button type="button" class="btn-close" aria-label="Close"></button>

Bootstrap's own .btn-close already includes aria-label="Close" by convention — follow that pattern for your custom icon buttons too.

3. Form Labels Must Be Properly Associated

<!-- Bad: placeholder is not a label -->
<input type="email" class="form-control" placeholder="Email Address">

<!-- Good: label linked via for/id -->
<label for="emailInput" class="form-label">Email Address</label>
<input type="email" class="form-control" id="emailInput">

<!-- Good: visually hidden label for inline forms -->
<label for="searchInput" class="visually-hidden">Search</label>
<input type="search" class="form-control" id="searchInput" placeholder="Search...">

Placeholders disappear when users type — they're not a substitute for labels. Screen readers announce the label, not the placeholder, when a field receives focus.

4. Form Validation Errors Need ARIA

<div class="mb-3">
  <label for="emailField" class="form-label">Email</label>
  <input
    type="email"
    class="form-control is-invalid"
    id="emailField"
    aria-describedby="emailError"
    aria-invalid="true"
  >
  <div id="emailError" class="invalid-feedback">
    Please enter a valid email address.
  </div>
</div>

aria-describedby links the input to its error message. aria-invalid="true" tells screen readers the field currently has an error — update this dynamically as validation state changes.

5. Navbar Toggler Needs Proper ARIA

Bootstrap's navbar toggler button includes good defaults — verify yours matches this pattern:

<button
  class="navbar-toggler"
  type="button"
  data-bs-toggle="collapse"
  data-bs-target="#navMenu"
  aria-controls="navMenu"
  aria-expanded="false"
  aria-label="Toggle navigation"
>
  <span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="navMenu">
  <!-- nav links -->
</div>

aria-expanded should toggle between true/false — Bootstrap's JS handles this automatically, just don't remove the attribute.

6. Modal Accessibility

Bootstrap modals handle focus trapping and Escape-to-close automatically. You need to provide proper labelling:

<div class="modal fade" id="confirmModal" tabindex="-1"
  aria-labelledby="confirmModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="confirmModalLabel">Confirm Deletion</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        Are you sure you want to delete this template?
      </div>
      <div class="modal-footer">
        <button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
        <button class="btn btn-danger">Delete</button>
      </div>
    </div>
  </div>
</div>

aria-labelledby must point to the id of the visible title — this is what screen readers announce when the modal opens. Bootstrap automatically returns focus to whatever triggered the modal when it closes.

7. Don't Remove Focus Indicators

A common but harmful pattern: removing Bootstrap's default focus ring for visual reasons.

/* Bad: removes keyboard navigation visibility entirely */
*:focus { outline: none; }
.btn:focus { box-shadow: none; }
/* Good: customize the focus style, don't remove it */
.btn:focus {
  box-shadow: 0 0 0 3px rgba(253, 71, 102, 0.4);
}

Keyboard users rely entirely on the focus indicator to know where they are on the page. Removing it makes the site unusable without a mouse.

8. Color Contrast

Bootstrap's default text colors generally pass WCAG AA, but verify your custom brand colors:

<!-- Check this against white backgrounds -->
<p style="color: #fd4766;">Brand colored text</p>

#fd4766 on white background has a contrast ratio of approximately 3.5:1 — this fails WCAG AA for normal text (needs 4.5:1) but passes for large text/headings (needs 3:1). Use it for headings and large UI elements, not small body text.

<!-- Better: use the brand color for large text, muted gray for body -->
<h2 style="color: #fd4766;">Marvel Dashboard</h2>
<p class="text-muted">Long descriptive text uses Bootstrap's accessible default gray.</p>

Check contrast with Chrome DevTools — inspect an element, look at the color picker, and it shows the contrast ratio and pass/fail against WCAG AA/AAA.

9. Skip Navigation Link

Add a skip link so keyboard users can bypass the navbar and jump straight to content:

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

<nav class="navbar navbar-expand-lg">...</nav>

<main id="main-content">
  <!-- page content -->
</main>

visually-hidden-focusable is a Bootstrap utility that hides the element visually but shows it when it receives keyboard focus — exactly the behavior a skip link needs.

10. Tables Need Proper Headers

<table class="table">
  <caption>Template sales by month</caption>
  <thead>
    <tr>
      <th scope="col">Month</th>
      <th scope="col">Sales</th>
      <th scope="col">Revenue</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">January</th>
      <td>120</td>
      <td>$3,480</td>
    </tr>
  </tbody>
</table>

scope="col" and scope="row" tell screen readers which header applies to which data cells — without this, a screen reader user navigating cell by cell loses context of what each number means.

11. Carousel Accessibility

Auto-playing carousels are a known accessibility problem — moving content can disorient users with cognitive or vestibular conditions:

<div id="heroCarousel" class="carousel slide"
  data-bs-ride="false"
  aria-label="Featured templates carousel">

  <div class="carousel-indicators">
    <button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="0"
      class="active" aria-current="true" aria-label="Slide 1"></button>
    <button type="button" data-bs-target="#heroCarousel" data-bs-slide-to="1"
      aria-label="Slide 2"></button>
  </div>

  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="slide1.webp" class="d-block w-100" alt="Marvel Angular Dashboard preview">
    </div>
  </div>

  <button class="carousel-control-prev" type="button"
    data-bs-target="#heroCarousel" data-bs-slide="prev">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Previous</span>
  </button>
  <button class="carousel-control-next" type="button"
    data-bs-target="#heroCarousel" data-bs-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Next</span>
  </button>
</div>

Set data-bs-ride="false" to disable autoplay — let users control when slides change. Bootstrap's default carousel markup already includes the visually-hidden text for prev/next buttons; don't remove it.

Testing Your Site

Keyboard test: unplug your mouse. Tab through the entire page. Every interactive element should be reachable and show a visible focus state. Modals should trap focus. Escape should close dropdowns and modals.

Screen reader test: turn on VoiceOver (Mac, Cmd+F5) or NVDA (Windows, free download) and navigate your site without looking at the screen. Notice if anything is confusing or unannounced.

Automated check: run Lighthouse's Accessibility audit in Chrome DevTools. It catches missing alt text, poor contrast, missing labels and ARIA misuse — not everything, but a strong first pass.

☐ All interactive elements are real button/a tags
☐ Icon-only buttons have aria-label
☐ Form inputs have associated labels (not just placeholders)
☐ Focus indicators are visible, not removed
☐ Color contrast passes WCAG AA (4.5:1 normal text, 3:1 large text)
☐ Modals have aria-labelledby pointing to the title
☐ Tables use scope="col"/"row" on headers
☐ Carousels don't autoplay, have visible controls
☐ Skip link present for keyboard users
☐ Tested with keyboard-only navigation

Frequently Asked Questions

Bootstrap 5's components include reasonable ARIA defaults and keyboard support out of the box — modals trap focus, dropdowns support arrow keys, and most components have correct roles. But you still need to add proper labels, alt text, and semantic structure yourself; Bootstrap can't do that for you.
Use real <button> elements, not styled divs. If a button only has an icon, add an aria-label: <button class='btn' aria-label='Close menu'><i class='bi bi-x'></i></button>. Ensure focus states are visible — don't remove Bootstrap's default focus ring with custom CSS.
Bootstrap 5 modals handle most of this automatically — focus trapping, Escape to close, and returning focus to the trigger element on close. You need to add aria-labelledby pointing to the modal title id, and ensure the modal has a proper heading.
Use Chrome DevTools' Lighthouse accessibility audit, or the WebAIM Contrast Checker. Bootstrap's default colors (text-muted at #6c757d on white) pass WCAG AA for normal text but check your own brand color overrides — many bright colors fail contrast on white backgrounds.

Need a Full Bootstrap 5 Admin Dashboard?

Get a complete Angular 21 + Bootstrap 5 dashboard with 50+ components — built by the same team behind BootstrapPlanet.

Browse Templates →

Use code FIRST30 for 30% off your first purchase.

Related Components