Alerts, Badges, Spinners & Progress

Bootstrap · Lesson 14 of 28 6 min read Updated

Users need to know what is happening: did the payment go through, how many messages are unread, is the page still loading, how far along is the upload? Bootstrap has four small components for this kind of feedback: alerts, badges, spinners and progress bars.

In this lesson you will use all four, make alerts that can be closed or added from JavaScript, and learn the accessibility attributes each one needs.

Alerts

An alert is a coloured message box. Use alert plus a colour variant, and role="alert" when the message appears as a result of something the user did, so screen readers read it out.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Alerts</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-3">
    <div class="alert alert-success" role="alert">
      <h2 class="alert-heading h5">Payment received</h2>
      <p class="mb-0">₹2,400 for the November tiffin plan. <a href="#receipt" class="alert-link">Download receipt</a>.</p>
    </div>

    <div class="alert alert-warning alert-dismissible fade show" role="alert">
      Your bus pass expires in <strong>3 days</strong>.
      <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    </div>

    <div class="alert alert-info">Exam timetable for Class 12 will be shared on Monday.</div>

    <button type="button" class="btn btn-primary" id="saveBtn">Save profile</button>
    <div id="messages" class="mt-3"></div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    // Build an alert from JavaScript, as a "live" message after an action.
    function showAlert(text, type) {
      const box = document.createElement("div");
      box.className = `alert alert-${type} alert-dismissible fade show`;
      box.setAttribute("role", "alert");
      box.textContent = text; // textContent, so user data can never inject HTML
      const close = document.createElement("button");
      close.type = "button";
      close.className = "btn-close";
      close.setAttribute("data-bs-dismiss", "alert");
      close.setAttribute("aria-label", "Close");
      box.append(close);
      document.getElementById("messages").append(box);
    }
    document.getElementById("saveBtn").addEventListener("click", () => {
      showAlert("Profile saved for Ananya.", "success");
    });
  </script>
</body>
</html>

The parts of an alert:

  • alert-heading styles a heading inside the alert, and alert-link gives links a matching, bolder colour.
  • A dismissible alert needs alert-dismissible (it makes room for the X), a btn-close button with data-bs-dismiss="alert" and an aria-label, and fade show for a smooth fade when it closes.
  • Alerts created by JavaScript work with data-bs-dismiss automatically, because Bootstrap listens for clicks on the whole document.

You can also close an alert from code with bootstrap.Alert.getOrCreateInstance(el).close(), and react to the closed.bs.alert event, for example to move keyboard focus to a sensible place after the alert disappears.

Badges

A badge is a small label or counter. Add badge plus a text-bg-* colour class. Badges scale with their parent, so a badge inside an <h2> is bigger than one inside a paragraph.

HTML
<h2 class="h5">Inbox <span class="badge text-bg-secondary">4</span></h2>

<span class="badge rounded-pill text-bg-success">In stock</span>
<span class="badge rounded-pill text-bg-danger">Sold out</span>

<button type="button" class="btn btn-primary position-relative">
  Cart
  <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill text-bg-danger">
    3 <span class="visually-hidden">items in cart</span>
  </span>
</button>

rounded-pill gives a fully rounded shape. The cart button uses the position helpers from Display & Flex Utilities to pin the counter to the corner. A bare "3" means nothing to a screen reader, so a visually-hidden span explains it.

Spinners

A spinner shows that something is loading when you cannot tell how long it will take. There are two styles: spinner-border (a spinning ring) and spinner-grow (a pulsing dot). Colour them with text-* classes and shrink them with spinner-border-sm or spinner-grow-sm.

The spinner itself is just a picture, so give it role="status" and a hidden text such as "Loading...". Inside a button, hide the spinner from screen readers with aria-hidden="true" and let the button text explain:

HTML
<div class="spinner-border text-primary" role="status">
  <span class="visually-hidden">Loading results...</span>
</div>

<button class="btn btn-success" type="button" disabled>
  <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
  <span role="status">Processing payment...</span>
</button>

Disable the button while the request runs, so an impatient user cannot pay twice.

Progress bars

When you do know how far along something is (an upload, a course, a quiz), show a progress bar. In Bootstrap 5.3 the outer progress element carries the accessibility information, and the inner progress-bar shows the fill with its width:

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Progress</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-3">
    <p class="mb-1">Course completed</p>
    <div class="progress mb-3" role="progressbar" aria-label="Course completed" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">
      <div class="progress-bar" style="width: 65%">65%</div>
    </div>

    <p class="mb-1">Storage used (thin bar)</p>
    <div class="progress mb-3" role="progressbar" aria-label="Storage used" aria-valuenow="90" aria-valuemin="0" aria-valuemax="100" style="height: 6px">
      <div class="progress-bar bg-danger" style="width: 90%"></div>
    </div>

    <p class="mb-1">Monthly budget: rent, food, travel</p>
    <div class="progress-stacked mb-3">
      <div class="progress" role="progressbar" aria-label="Rent" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100" style="width: 45%">
        <div class="progress-bar">Rent</div>
      </div>
      <div class="progress" role="progressbar" aria-label="Food" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" style="width: 25%">
        <div class="progress-bar bg-success">Food</div>
      </div>
      <div class="progress" role="progressbar" aria-label="Travel" aria-valuenow="15" aria-valuemin="0" aria-valuemax="100" style="width: 15%">
        <div class="progress-bar text-bg-warning">Travel</div>
      </div>
    </div>

    <p class="mb-1">Uploading marksheet.pdf</p>
    <div class="progress mb-2" role="progressbar" aria-label="Upload" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" id="upload">
      <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0%"></div>
    </div>
    <button type="button" class="btn btn-outline-primary btn-sm" id="startBtn">Start upload</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    const upload = document.getElementById("upload");
    const bar = upload.querySelector(".progress-bar");
    document.getElementById("startBtn").addEventListener("click", () => {
      let done = 0;
      const timer = setInterval(() => {
        done = Math.min(done + 10, 100);
        bar.style.width = done + "%";
        bar.textContent = done + "%";
        upload.setAttribute("aria-valuenow", done); // keep screen readers in sync
        if (done === 100) clearInterval(timer);
      }, 300);
    });
  </script>
</body>
</html>

Things to notice:

  • The width style on progress-bar decides how full it looks; the aria-valuenow on progress tells screen readers the number. When you update one in JavaScript, update both.
  • Set the height on the outer progress element for thin bars.
  • progress-stacked holds several progress elements side by side; there, each progress gets the width.
  • progress-bar-striped adds stripes, and progress-bar-animated makes them move. Use the animation only while work is actually happening.
  • Colour bars with bg-* or text-bg-* classes. text-bg-warning also makes the label text dark enough to read.

Common mistakes

  • A dismissible alert without the JavaScript bundle. The X shows but clicking it does nothing.
  • Building alerts with innerHTML and user input. A name like <img onerror=...> would run as code. Use textContent, as in the example.
  • Spinners and counters without hidden text. A screen reader hears nothing, or just "3". Add visually-hidden text or role="status".
  • Updating only the width of a progress bar. The visual and the aria-valuenow value then disagree.

Key points

  • Alerts are alert alert-{color}; add alert-dismissible fade show and a btn-close with data-bs-dismiss="alert" to make them closable.
  • Use role="alert" for messages that appear after an action, and build dynamic alerts with textContent.
  • Badges are badge text-bg-{color}, optionally rounded-pill, and can be pinned to a corner with position helpers.
  • Spinners (spinner-border, spinner-grow) need role="status" and hidden text.
  • In 5.3, progress bars put role="progressbar" and aria-value* on the outer progress, and the fill width on progress-bar.

Exercise

Make a "Results are loading" demo for a college website. Show a spinner with hidden text. After 2 seconds (use setTimeout), remove the spinner and show a green dismissible alert saying "Results published for 1,240 students" with a badge reading "New", plus a progress bar showing that 82% of students passed.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Results</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-3">
    <div class="spinner-border text-primary" role="status" id="loader">
      <span class="visually-hidden">Loading results...</span>
    </div>
    <div id="result" hidden>
      <div class="alert alert-success alert-dismissible fade show" role="alert">
        Results published for 1,240 students <span class="badge text-bg-success">New</span>
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
      </div>
      <p class="mb-1">Pass percentage</p>
      <div class="progress" role="progressbar" aria-label="Pass percentage" aria-valuenow="82" aria-valuemin="0" aria-valuemax="100">
        <div class="progress-bar bg-success" style="width: 82%">82%</div>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    setTimeout(() => {
      document.getElementById("loader").remove();
      document.getElementById("result").hidden = false;
    }, 2000);
  </script>
</body>
</html>

The spinner has role="status" and hidden text while waiting. After 2 seconds the script removes it and reveals the alert and the progress bar, whose aria-valuenow matches its width.

Quick Quiz

1. What makes an alert closable with an X button?

2. In Bootstrap 5.3, which element carries role="progressbar" and aria-valuenow?

3. Which class replaced Bootstrap 4's badge-pill?

4. Why build a dynamic alert's message with textContent instead of innerHTML?

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