Alerts, Badges, Spinners & Progress
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.
<!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-headingstyles a heading inside the alert, andalert-linkgives links a matching, bolder colour.- A dismissible alert needs
alert-dismissible(it makes room for the X), abtn-closebutton withdata-bs-dismiss="alert"and anaria-label, andfade showfor a smooth fade when it closes. - Alerts created by JavaScript work with
data-bs-dismissautomatically, 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.
<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:
<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:
<!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
widthstyle onprogress-bardecides how full it looks; thearia-valuenowonprogresstells screen readers the number. When you update one in JavaScript, update both. - Set the height on the outer
progresselement for thin bars. progress-stackedholds severalprogresselements side by side; there, eachprogressgets the width.progress-bar-stripedadds stripes, andprogress-bar-animatedmakes them move. Use the animation only while work is actually happening.- Colour bars with
bg-*ortext-bg-*classes.text-bg-warningalso 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
innerHTMLand user input. A name like<img onerror=...>would run as code. UsetextContent, as in the example. - Spinners and counters without hidden text. A screen reader hears nothing, or just "3". Add
visually-hiddentext orrole="status". - Updating only the width of a progress bar. The visual and the
aria-valuenowvalue then disagree.
Key points
- Alerts are
alert alert-{color}; addalert-dismissible fade showand abtn-closewithdata-bs-dismiss="alert"to make them closable. - Use
role="alert"for messages that appear after an action, and build dynamic alerts withtextContent. - Badges are
badge text-bg-{color}, optionallyrounded-pill, and can be pinned to a corner with position helpers. - Spinners (
spinner-border,spinner-grow) needrole="status"and hidden text. - In 5.3, progress bars put
role="progressbar"andaria-value*on the outerprogress, and the fill width onprogress-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
<!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?
The dismissible class makes room for the button, and the data attribute tells Bootstrap's JavaScript to close it.
2. In Bootstrap 5.3, which element carries role="progressbar" and aria-valuenow?
Version 5.3 moved the role and values to the wrapper, while the bar shows the fill.
3. Which class replaced Bootstrap 4's badge-pill?
Bootstrap 5 uses the general rounded-pill utility for pill-shaped badges.
4. Why build a dynamic alert's message with textContent instead of innerHTML?
Inserting user input with innerHTML can run attacker-controlled code (XSS).
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
