Skip to content

Documentation

On this page

Spinner

Set aria-busy="true" on the host that is loading. CSS paints the disc. Remove the attribute when the work is done. There is no extra .spinner-border child and no spinner plugin. A known percent is a <progress>.

Sizes are minified and gzip. A plugin figure includes the Bootstrap runtime that plugin imports. Shipping every plugin is bootstrap.bundle.min.js at 23.3 KB gzip, not the sum of these rows.
What runs it JavaScript
Bootstrap 5.3 No Spinner plugin. Markup is .spinner-border plus visually-hidden text. Not in bootstrap.min.js
This repo `aria-busy="true"` on the host. CSS paints the disc. 0 KB
Markup this component accepts. The full inventory is /components.json.
Avoid
  • Do not put a .spinner-border child inside a host that already has aria-busy. The disc is a pseudo-element.
  • A known percent is a <progress>, not a spinner. See Progress.
  • Do not set aria-busy on body for one widget. Put it on the host that is changing.

How it works

  • The attribute is the state. CSS matches [aria-busy="true"]. Your app sets it and clears it. This package ships no script for that.
  • A block host hides its children visually and centers a disc. Keep the children’s box so the layout does not collapse.
  • A <button> or .btn keeps its name. The disc is a small ::before at .spinner-border-sm size. Add disabled so a second click does not submit.
  • An inline <span> keeps its text and shows a smaller disc ahead of it.
  • prefers-reduced-motion slows the turn. It does not freeze the disc.
  • .spinner-border remains in the stylesheet if you need a disc that is not a busy host. Do not put one inside a host that already has aria-busy.

Toggle busy on each example is a docs demo, not required markup.

Buttons

The idle control and the busy one share a name. disabled blocks a second press.

HTML
<div class="d-flex flex-wrap gap-2">
  <button type="button" class="btn btn-primary">Publish</button>
  <button type="button" class="btn btn-primary" aria-busy="true" disabled>Publish</button>
  <button type="button" class="btn btn-outline-secondary" aria-busy="true" disabled>Export CSS</button>
  <button type="button" class="btn btn-sm btn-primary" aria-busy="true" disabled>Publish</button>
</div>

Card

role=status names the host while the body is visually hidden. The box stays.

Field sizing

The control grows to its contents.

Field sizing

The control grows to its contents.

HTML
<div class="row g-3">
  <div class="col-md-6">
    <div class="card h-100" role="status" aria-busy="true" aria-label="Loading page notes">
      <div class="card-body">
        <h3 class="card-title h5">Field sizing</h3>
        <p class="card-text mb-0">The control grows to its contents.</p>
      </div>
    </div>
  </div>
  <div class="col-md-6">
    <div class="card h-100">
      <div class="card-body">
        <h3 class="card-title h5">Field sizing</h3>
        <p class="card-text mb-0">The control grows to its contents.</p>
      </div>
    </div>
  </div>
</div>

Inline

The status text stays. A small disc sits before it.

  • Compiling Sass…
  • Writing changelog…
  • Inventory updated
HTML
<ul class="list-unstyled mb-0">
  <li class="mb-2"><span aria-busy="true">Compiling Sass…</span></li>
  <li class="mb-2"><span aria-busy="true">Writing changelog…</span></li>
  <li>Inventory updated</li>
</ul>

Table

Put aria-busy on the table. Rows keep their height under the disc.

Page Baseline
Tooltip Newly
Carousel Experimental
HTML
<table class="table mb-0" aria-busy="true" aria-label="Loading catalog">
  <thead>
    <tr>
      <th scope="col">Page</th>
      <th scope="col">Baseline</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tooltip</td>
      <td>Newly</td>
    </tr>
    <tr>
      <td>Carousel</td>
      <td>Experimental</td>
    </tr>
  </tbody>
</table>

Live region

Assistive technology waits until you clear aria-busy before announcing the update.

Indexing the catalog…

HTML
<div class="alert alert-info mb-0" role="status" aria-live="polite" aria-busy="true">
  <p class="mb-0">Indexing the catalog…</p>
</div>

Standalone

An empty host still needs a name. min-block-size comes from CSS when there are no children.

HTML
<div role="status" aria-busy="true" aria-label="Loading catalog"></div>

Accessibility

The attribute is the state. The disc is decorative (::before / ::after). Do not add role="status" on a spinner element.

A nameless host needs a role that can take a name (role="status") and aria-label (or a more specific name) while it is busy. A generic <div> with only aria-label is ignored.

A button keeps its accessible name. disabled blocks a second submit.

On a live region, aria-busy="true" tells assistive technology to wait until you set it to false (or remove it) before announcing the update.

prefers-reduced-motion slows the turn. It does not freeze the disc, so the busy state stays visible.

Browser support

Three columns, not one baseline. The same row is on the overview.
No script Polyfill No fallback
aria-busy is a global ARIA state. The disc is CSS on [aria-busy="true"]. Removing the attribute hides it. None Without this CSS the attribute still tells assistive technology. There is no disc. .spinner-border stays in the sheet as a standalone glyph.