Bootstrap Modal: 16 Examples + Options, Events and Fixes

Paste a Bootstrap 4 modal into a Bootstrap 5 page and the button just sits there. Bootstrap 5 prefixed every data attribute with bs, so data-toggle="modal" does nothing while data-bs-toggle="modal" works.

Below are 16 Bootstrap modal examples you can copy, followed by a guide to the markup, options, events, styling, accessibility and the bugs that stop a modal from opening.

By Bogdan Sandu ยท Updated October 29, 2026

Every example runs on Bootstrap 5.3.8 from the jsDelivr CDN. Add these tags to your page, then paste an example's HTML, CSS and JS.

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.2/font/bootstrap-icons.min.css" rel="stylesheet" integrity="sha384-nWN3KqaaVcbEsCjLSV+2jMjh62Iny/+mN3srM8TZeM8N6aj7eDl5wwYggjAJ4/sJ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

Confirmation and alert dialogs

Short modals that ask for one decision: delete or keep, leave or stay, accept or adjust. They lean on the backdrop, keyboard and focus behavior Bootstrap ships with.

Delete confirmation

  • modal-sm
  • modal-dialog-centered
  • shown.bs.modal
  • bg-danger-subtle

Click the button to open the modal.

A small centered modal with no header, a warning icon and stacked buttons. The shown.bs.modal listener moves focus to Cancel, the least destructive choice, as the ARIA dialog pattern recommends. The delete button hides the modal through the JS API and updates a status line.

View code
<div class="text-center">
  <button type="button" class="btn btn-outline-danger bmd-del-open" data-bs-toggle="modal" data-bs-target="#bmd-del"><i class="bi bi-trash3 me-2" aria-hidden="true"></i>Delete project</button>
  <p class="small text-body-secondary mt-3 mb-0 bmd-del-status" role="status">Spring launch, 24 files</p>
</div>
<div class="modal fade" id="bmd-del" tabindex="-1" aria-labelledby="bmd-del-title" aria-describedby="bmd-del-desc" aria-hidden="true">
  <div class="modal-dialog modal-sm modal-dialog-centered">
    <div class="modal-content text-center">
      <div class="modal-body p-4">
        <span class="bmd-del-icon bg-danger-subtle text-danger-emphasis rounded-circle d-inline-flex align-items-center justify-content-center mb-3"><i class="bi bi-exclamation-triangle" aria-hidden="true"></i></span>
        <h1 class="modal-title fs-5 mb-2" id="bmd-del-title">Delete Spring launch?</h1>
        <p class="text-body-secondary mb-4" id="bmd-del-desc">All 24 files and the share links go with it. This cannot be undone.</p>
        <div class="d-grid gap-2">
          <button type="button" class="btn btn-danger bmd-del-confirm">Delete project</button>
          <button type="button" class="btn btn-light bmd-del-cancel" data-bs-dismiss="modal">Cancel</button>
        </div>
      </div>
    </div>
  </div>
</div>

Static backdrop: unsaved changes

  • data-bs-backdrop="static"
  • data-bs-keyboard="false"
  • hidePrevented.bs.modal

Open it, then press Escape or click outside.

With a static backdrop and the keyboard option off, a click outside or the Escape key cannot close the dialog; Bootstrap plays its short bounce instead. The hidePrevented.bs.modal listener adds a hint so the user knows they have to pick an answer.

View code
<button type="button" class="btn btn-dark bmd-unsaved-open" data-bs-toggle="modal" data-bs-target="#bmd-unsaved"><i class="bi bi-x-lg me-2" aria-hidden="true"></i>Close editor</button>
<div class="modal fade" id="bmd-unsaved" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="bmd-unsaved-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-unsaved-title">Leave without saving?</h1>
      </div>
      <div class="modal-body">
        <p class="mb-2">You changed 3 fields in <strong>Invoice #1042</strong> since the last save.</p>
        <p class="small text-warning-emphasis bg-warning-subtle rounded px-2 py-1 mb-0 d-none bmd-unsaved-hint" role="status">Choose an option to continue.</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-link link-danger text-decoration-none me-auto" data-bs-dismiss="modal">Discard</button>
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Keep editing</button>
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Save and close</button>
      </div>
    </div>
  </div>
</div>

Dark upgrade dialog on a light page

  • data-bs-theme="dark"
  • --bs-btn-*
  • list-unstyled

Click the button to open the modal.

The page stays light while data-bs-theme="dark" on the modal switches the dialog, its text and its close button to the dark palette, which is how Bootstrap 5.3 color modes work per component. The amber call to action is a btn-warning restyled through its --bs-btn-* variables.

View code
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#bmd-upgrade"><i class="bi bi-stars me-2" aria-hidden="true"></i>See Pro features</button>
<div class="modal fade" id="bmd-upgrade" data-bs-theme="dark" tabindex="-1" aria-labelledby="bmd-upgrade-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content bmd-upgrade-box">
      <div class="modal-header border-0 pb-0">
        <h1 class="modal-title fs-4" id="bmd-upgrade-title">Ledgerly Pro</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <p class="text-body-secondary">Everything in Starter, plus the tools growing teams ask for.</p>
        <ul class="list-unstyled d-grid gap-2 mb-4">
          <li><i class="bi bi-check2-circle text-warning me-2" aria-hidden="true"></i>Unlimited clients and invoices</li>
          <li><i class="bi bi-check2-circle text-warning me-2" aria-hidden="true"></i>Recurring billing and reminders</li>
          <li><i class="bi bi-check2-circle text-warning me-2" aria-hidden="true"></i>Multi-currency reports</li>
        </ul>
        <p class="mb-0"><span class="fs-2 fw-bold">$19</span><span class="text-body-secondary"> per month, billed yearly</span></p>
      </div>
      <div class="modal-footer border-0 pt-0">
        <button type="button" class="btn btn-link text-body-secondary text-decoration-none" data-bs-dismiss="modal">Maybe later</button>
        <button type="button" class="btn btn-warning bmd-upgrade-cta" data-bs-dismiss="modal">Start 14-day trial</button>
      </div>
    </div>
  </div>
</div>

Forms inside modals

Sign-up, sign-in and multi-step flows that live in a dialog. Each one uses Bootstrap form controls, the validation classes and a footer button tied to the form with the form attribute.

Newsletter signup with validation

  • needs-validation
  • was-validated
  • form attribute
  • invalid-feedback

Open the modal, then try the form.

The form lives in modal-body while the submit button sits in the footer, so the button points at the form with form="bmd-news-form". Its click handler runs checkValidity(), adds was-validated to show the Bootstrap feedback styles, and swaps a valid form for a thank-you message; replace preventDefault() with your real request.

View code
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#bmd-news"><i class="bi bi-envelope-paper me-2" aria-hidden="true"></i>Get the weekly digest</button>
<div class="modal fade" id="bmd-news" tabindex="-1" aria-labelledby="bmd-news-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-news-title">The Greenhouse Weekly</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <form id="bmd-news-form" class="needs-validation" novalidate>
          <p class="text-body-secondary small">One email every Friday with planting dates, pest alerts and a seasonal recipe.</p>
          <div class="mb-3">
            <label for="bmd-news-name" class="form-label">First name</label>
            <input type="text" class="form-control" id="bmd-news-name" autocomplete="given-name" required>
            <div class="invalid-feedback">Tell us what to call you.</div>
          </div>
          <div class="mb-3">
            <label for="bmd-news-email" class="form-label">Email</label>
            <input type="email" class="form-control" id="bmd-news-email" placeholder="you@example.com" autocomplete="email" required>
            <div class="invalid-feedback">Enter a valid email address.</div>
          </div>
          <div class="form-check">
            <input class="form-check-input" type="checkbox" id="bmd-news-ok" required>
            <label class="form-check-label small" for="bmd-news-ok">I agree to receive the newsletter</label>
          </div>
        </form>
        <div class="text-center py-3 d-none bmd-news-done" role="status">
          <i class="bi bi-check-circle-fill text-success fs-1" aria-hidden="true"></i>
          <p class="fw-semibold mt-2 mb-0 bmd-news-thanks">You are on the list.</p>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">No thanks</button>
        <button type="submit" class="btn btn-success bmd-news-submit" form="bmd-news-form">Subscribe</button>
      </div>
    </div>
  </div>
</div>

Sign-in modal with password toggle

  • form-floating
  • input-group
  • aria-pressed
  • shown.bs.modal

Open the modal, then try the form.

Floating labels, an input group with a show-password button and a full-width submit, all standard Bootstrap form parts. The toggle flips the input type and aria-pressed, and shown.bs.modal puts the cursor in the email field because autofocus does not work inside a Bootstrap modal.

View code
<button type="button" class="btn btn-light" data-bs-toggle="modal" data-bs-target="#bmd-login"><i class="bi bi-person-circle me-2" aria-hidden="true"></i>Sign in</button>
<div class="modal fade" id="bmd-login" tabindex="-1" aria-labelledby="bmd-login-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered bmd-login-dialog">
    <div class="modal-content">
      <div class="modal-header border-0 pb-0">
        <h1 class="modal-title fs-4 fw-bold" id="bmd-login-title">Welcome back</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body pt-2">
        <p class="text-body-secondary small">Sign in to Northbeam to see your dashboards.</p>
        <form id="bmd-login-form" novalidate>
          <div class="form-floating mb-3">
            <input type="email" class="form-control" id="bmd-login-email" placeholder="name@example.com" autocomplete="username" required>
            <label for="bmd-login-email">Email address</label>
          </div>
          <div class="input-group mb-3">
            <div class="form-floating">
              <input type="password" class="form-control" id="bmd-login-pass" placeholder="Password" autocomplete="current-password" required>
              <label for="bmd-login-pass">Password</label>
            </div>
            <button type="button" class="btn btn-outline-secondary bmd-login-eye" aria-pressed="false" aria-controls="bmd-login-pass"><i class="bi bi-eye" aria-hidden="true"></i><span class="visually-hidden">Show password</span></button>
          </div>
          <div class="d-flex justify-content-between align-items-center mb-3">
            <div class="form-check">
              <input class="form-check-input" type="checkbox" id="bmd-login-keep">
              <label class="form-check-label small" for="bmd-login-keep">Keep me signed in</label>
            </div>
            <a href="#" class="small">Forgot password?</a>
          </div>
          <button type="submit" class="btn btn-primary w-100 py-2">Sign in</button>
        </form>
      </div>
    </div>
  </div>
</div>

Multi-step wizard

  • progress
  • d-none
  • btn-check
  • hidden.bs.modal

Open it and click Next.

Three steps share one modal-body and a progress bar; the script toggles d-none, updates aria-valuenow and swaps the button labels. Step choices use btn-check radio buttons, and hidden.bs.modal resets the wizard so it starts at step one next time.

View code
<button type="button" class="btn btn-primary bmd-wiz-open" data-bs-toggle="modal" data-bs-target="#bmd-wiz"><i class="bi bi-plus-lg me-2" aria-hidden="true"></i>New workspace</button>
<div class="modal fade" id="bmd-wiz" tabindex="-1" aria-labelledby="bmd-wiz-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-wiz-title">Create a workspace</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <div class="d-flex justify-content-between small text-body-secondary mb-1"><span class="bmd-wiz-label">Step 1 of 3</span><span class="bmd-wiz-name">Name</span></div>
        <div class="progress mb-4 bmd-wiz-progress" role="progressbar" aria-label="Setup progress" aria-valuenow="33" aria-valuemin="0" aria-valuemax="100" style="height: 6px">
          <div class="progress-bar bmd-wiz-bar" style="width: 33%"></div>
        </div>
        <div class="bmd-step" data-bmd-name="Name">
          <label for="bmd-wiz-ws" class="form-label">Workspace name</label>
          <input type="text" class="form-control" id="bmd-wiz-ws" value="Harbor Studio">
          <div class="form-text">Shown to everyone you invite.</div>
        </div>
        <div class="bmd-step d-none" data-bmd-name="Team size">
          <p class="form-label mb-2" id="bmd-wiz-size">How many people will use it?</p>
          <div class="d-grid gap-2 bmd-wiz-grid" role="group" aria-labelledby="bmd-wiz-size">
            <input type="radio" class="btn-check" name="bmd-wiz-size" id="bmd-wiz-s1" checked>
            <label class="btn btn-outline-primary" for="bmd-wiz-s1">Just me</label>
            <input type="radio" class="btn-check" name="bmd-wiz-size" id="bmd-wiz-s2">
            <label class="btn btn-outline-primary" for="bmd-wiz-s2">2 to 10</label>
            <input type="radio" class="btn-check" name="bmd-wiz-size" id="bmd-wiz-s3">
            <label class="btn btn-outline-primary" for="bmd-wiz-s3">11 or more</label>
          </div>
        </div>
        <div class="bmd-step d-none" data-bmd-name="Invite">
          <label for="bmd-wiz-mail" class="form-label">Invite teammates</label>
          <textarea class="form-control" id="bmd-wiz-mail" rows="3" placeholder="One email per line"></textarea>
        </div>
      </div>
      <div class="modal-footer justify-content-between">
        <button type="button" class="btn btn-outline-secondary bmd-wiz-back" disabled>Back</button>
        <button type="button" class="btn btn-primary bmd-wiz-next">Next</button>
      </div>
    </div>
  </div>
</div>

Fullscreen on phones

  • modal-fullscreen-sm-down
  • row g-3
  • form-select

Open the modal, then try the form.

modal-fullscreen-sm-down keeps a normal centered dialog from 576px up and covers the whole viewport below it, which gives a shipping form the room it needs on a phone. Compare the desktop and mobile screenshots: the markup is identical.

View code
<button type="button" class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#bmd-ship"><i class="bi bi-truck me-2" aria-hidden="true"></i>Edit shipping address</button>
<div class="modal fade" id="bmd-ship" tabindex="-1" aria-labelledby="bmd-ship-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-ship-title">Shipping address</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <form id="bmd-ship-form" class="row g-3">
          <div class="col-12">
            <label for="bmd-ship-name" class="form-label">Full name</label>
            <input type="text" class="form-control" id="bmd-ship-name" value="Mara Lindqvist" autocomplete="name">
          </div>
          <div class="col-12">
            <label for="bmd-ship-street" class="form-label">Street address</label>
            <input type="text" class="form-control" id="bmd-ship-street" value="14 Alder Lane" autocomplete="street-address">
          </div>
          <div class="col-sm-7">
            <label for="bmd-ship-city" class="form-label">City</label>
            <input type="text" class="form-control" id="bmd-ship-city" value="Portland" autocomplete="address-level2">
          </div>
          <div class="col-sm-5">
            <label for="bmd-ship-zip" class="form-label">ZIP code</label>
            <input type="text" class="form-control" id="bmd-ship-zip" value="97205" inputmode="numeric" autocomplete="postal-code">
          </div>
          <div class="col-12">
            <label for="bmd-ship-speed" class="form-label">Delivery</label>
            <select class="form-select" id="bmd-ship-speed">
              <option>Standard, 3 to 5 days</option>
              <option>Express, next business day</option>
            </select>
          </div>
        </form>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
        <button type="submit" class="btn btn-primary bmd-ship-save" form="bmd-ship-form">Save address</button>
      </div>
    </div>
  </div>
</div>

Media and long content

Galleries, video players, legal text and tabbed settings. These show the size classes, scrollable bodies and how other Bootstrap plugins behave inside an open modal.

Photo lightbox gallery

  • modal-lg
  • show.bs.modal
  • event.relatedTarget
  • ratio

Click a photo.

Three thumbnails open one modal-lg lightbox. The show.bs.modal listener reads event.relatedTarget, the thumbnail that was clicked, and copies its image source, alt text and caption into the modal before it appears. The modal content is made a dark photo frame with --bs-modal-bg, --bs-modal-border-width and --bs-modal-border-radius.

View code
<div class="d-flex gap-2 gap-sm-3 bmd-thumbs">
  <button type="button" class="bmd-thumb" data-bs-toggle="modal" data-bs-target="#bmd-lightbox" data-bmd-caption="Lysefjord seen from the Preikestolen cliff"><img src="fjord.webp" alt="Deep blue fjord between grey cliffs, with hikers on a rock plateau" width="800" height="600"></button>
  <button type="button" class="bmd-thumb" data-bs-toggle="modal" data-bs-target="#bmd-lightbox" data-bmd-caption="Canal houses and moored boats on a spring morning"><img src="canal-houses.webp" alt="Brick canal houses with white boats moored along a wooden quay" width="800" height="600"></button>
  <button type="button" class="bmd-thumb" data-bs-toggle="modal" data-bs-target="#bmd-lightbox" data-bmd-caption="Base camp below snow peaks after an overnight storm"><img src="snow-camp.webp" alt="Yellow tent half buried in snow beside a stone wall, with jagged peaks behind" width="800" height="600"></button>
</div>
<div class="modal fade bmd-lb" id="bmd-lightbox" tabindex="-1" aria-labelledby="bmd-lb-caption" aria-hidden="true">
  <div class="modal-dialog modal-lg modal-dialog-centered">
    <div class="modal-content">
      <div class="d-flex justify-content-end mb-2" data-bs-theme="dark">
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="ratio ratio-4x3 rounded-3 overflow-hidden">
        <img class="bmd-lb-img object-fit-cover" src="fjord.webp" alt="" width="800" height="600">
      </div>
      <p class="text-white mt-2 mb-0 px-1 small" id="bmd-lb-caption">Photo</p>
    </div>
  </div>
</div>

Video player modal

  • modal-lg
  • ratio-16x9
  • aria-pressed
  • hidden.bs.modal

Open it and press play.

A dark modal-lg with a 16:9 ratio box, where a real page would put its video iframe; here a still frame stands in for the player. Pressing play flips aria-pressed and the label, and the hidden.bs.modal listener resets the player, the same hook you would use to pause a real embed when the modal closes.

View code
<button type="button" class="btn btn-outline-light" data-bs-toggle="modal" data-bs-target="#bmd-video"><i class="bi bi-play-circle me-2" aria-hidden="true"></i>Watch the trailer</button>
<div class="modal fade" id="bmd-video" tabindex="-1" aria-labelledby="bmd-video-title" aria-hidden="true">
  <div class="modal-dialog modal-lg modal-dialog-centered">
    <div class="modal-content bmd-video-box">
      <div class="modal-header">
        <h1 class="modal-title fs-6" id="bmd-video-title">Coastline, a short film by Tern Studio</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="ratio ratio-16x9 bmd-video-stage">
        <img src="coast-road.webp" alt="Terraced green hillside dropping to a calm sea at sunset" class="object-fit-cover" width="800" height="600">
        <div class="d-flex flex-column justify-content-end">
          <div class="d-flex align-items-center gap-3 p-3 bmd-video-bar">
            <button type="button" class="btn btn-light rounded-circle bmd-video-play" aria-pressed="false"><i class="bi bi-play-fill" aria-hidden="true"></i><span class="visually-hidden">Play</span></button>
            <div class="progress flex-grow-1" role="progressbar" aria-label="Playback position" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="height: 4px">
              <div class="progress-bar bg-light bmd-video-pos" style="width: 0%"></div>
            </div>
            <span class="small text-white bmd-video-time">0:00 / 2:14</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Scrollable terms with read-to-accept

  • modal-dialog-scrollable
  • scroll event
  • disabled

Open it and scroll the text to the end.

modal-dialog-scrollable keeps the header and footer fixed and scrolls only modal-body. A scroll listener on the body enables the Accept button once the reader reaches the end, a common pattern for terms updates.

View code
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#bmd-terms"><i class="bi bi-file-earmark-text me-2" aria-hidden="true"></i>Review updated terms</button>
<div class="modal fade" id="bmd-terms" data-bs-backdrop="static" tabindex="-1" aria-labelledby="bmd-terms-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-terms-title">Terms of service, March update</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body bmd-terms-body" tabindex="0">
        <h2 class="fs-6">1. Your account</h2>
        <p>You are responsible for the activity on your Parcelpoint account and for keeping your sign-in details private. Tell us within 48 hours if you think someone else has used it.</p>
        <h2 class="fs-6">2. Shipments</h2>
        <p>Rates are calculated from the weight and size you enter. If the carrier measures a parcel differently, the difference is charged to the card on file and shown on your next statement.</p>
        <h2 class="fs-6">3. Prohibited items</h2>
        <p>Batteries over 100 Wh, aerosols, perishable food and anything listed on the carrier restricted list cannot be shipped through Parcelpoint labels.</p>
        <h2 class="fs-6">4. Refunds</h2>
        <p>Unused labels can be refunded for 30 days after purchase. Refunds go back to the original payment method within 10 business days.</p>
        <h2 class="fs-6">5. Changes to these terms</h2>
        <p class="mb-0">We will email you at least 14 days before a change that affects pricing or refunds takes effect.</p>
      </div>
      <div class="modal-footer">
        <span class="small text-body-secondary me-auto bmd-terms-note">Scroll to the end to continue</span>
        <button type="button" class="btn btn-primary bmd-terms-ok" data-bs-dismiss="modal" disabled>I accept</button>
      </div>
    </div>
  </div>
</div>

Settings modal with tabs

  • nav-underline
  • data-bs-toggle="tab"
  • tab-content
  • modal-lg

Open it and switch tabs.

The Tab plugin runs inside the modal with nothing extra: nav-underline buttons with data-bs-toggle="tab" switch the tab-pane elements. The modal-lg width gives three panes room on desktop, and the nav scrolls sideways on narrow screens instead of wrapping.

View code
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#bmd-settings"><i class="bi bi-gear me-2" aria-hidden="true"></i>Settings</button>
<div class="modal fade" id="bmd-settings" tabindex="-1" aria-labelledby="bmd-settings-title" aria-hidden="true">
  <div class="modal-dialog modal-lg modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header pb-0 border-0">
        <h1 class="modal-title fs-5" id="bmd-settings-title">Settings</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="px-3 border-bottom bmd-tabs-scroll">
        <ul class="nav nav-underline flex-nowrap" role="tablist">
          <li class="nav-item" role="presentation"><button class="nav-link active" id="bmd-tab-profile" data-bs-toggle="tab" data-bs-target="#bmd-pane-profile" type="button" role="tab" aria-controls="bmd-pane-profile" aria-selected="true">Profile</button></li>
          <li class="nav-item" role="presentation"><button class="nav-link bmd-tab-notify" id="bmd-tab-notify" data-bs-toggle="tab" data-bs-target="#bmd-pane-notify" type="button" role="tab" aria-controls="bmd-pane-notify" aria-selected="false">Notifications</button></li>
          <li class="nav-item" role="presentation"><button class="nav-link" id="bmd-tab-billing" data-bs-toggle="tab" data-bs-target="#bmd-pane-billing" type="button" role="tab" aria-controls="bmd-pane-billing" aria-selected="false">Billing</button></li>
        </ul>
      </div>
      <div class="modal-body tab-content">
        <div class="tab-pane fade show active" id="bmd-pane-profile" role="tabpanel" aria-labelledby="bmd-tab-profile" tabindex="0">
          <div class="mb-3"><label for="bmd-set-name" class="form-label">Display name</label><input type="text" class="form-control" id="bmd-set-name" value="Priya Raman"></div>
          <div><label for="bmd-set-tz" class="form-label">Time zone</label><select class="form-select" id="bmd-set-tz"><option>Europe/Berlin</option><option>America/Chicago</option></select></div>
        </div>
        <div class="tab-pane fade" id="bmd-pane-notify" role="tabpanel" aria-labelledby="bmd-tab-notify" tabindex="0">
          <div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" role="switch" id="bmd-set-n1" checked><label class="form-check-label" for="bmd-set-n1">Mentions and replies</label></div>
          <div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" role="switch" id="bmd-set-n2" checked><label class="form-check-label" for="bmd-set-n2">Weekly summary email</label></div>
          <div class="form-check form-switch"><input class="form-check-input" type="checkbox" role="switch" id="bmd-set-n3"><label class="form-check-label" for="bmd-set-n3">Product announcements</label></div>
        </div>
        <div class="tab-pane fade" id="bmd-pane-billing" role="tabpanel" aria-labelledby="bmd-tab-billing" tabindex="0">
          <p class="mb-1 fw-semibold">Team plan, 8 seats</p>
          <p class="text-body-secondary mb-0">Renews on 1 December for $96.</p>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Done</button>
      </div>
    </div>
  </div>
</div>

Theming and JavaScript patterns

One modal restyled only with --bs-modal-* variables, one modal shared by several buttons, two modals that swap, and a live log of the modal events in the order they fire.

Recipe card themed with --bs-modal-*

  • --bs-modal-bg
  • --bs-modal-border-radius
  • --bs-modal-footer-bg
  • modal.fade transform

Open the modal and watch what changes.

Every color, radius, border and padding change here is a --bs-modal-* variable set on one prefixed modal, so other modals on the page keep the defaults. The opening animation is swapped from the default slide-down to a gentle zoom by overriding the dialog transform for this modal only.

View code
<button type="button" class="btn btn-dark bmd-recipe-open" data-bs-toggle="modal" data-bs-target="#bmd-recipe"><i class="bi bi-journal-richtext me-2" aria-hidden="true"></i>Open recipe</button>
<div class="modal fade bmd-recipe" id="bmd-recipe" tabindex="-1" aria-labelledby="bmd-recipe-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <div>
          <p class="small text-uppercase fw-semibold mb-1 bmd-recipe-meta">Mains, 35 minutes</p>
          <h1 class="modal-title fs-3" id="bmd-recipe-title">Charred leek and white bean stew</h1>
        </div>
        <button type="button" class="btn-close align-self-start" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <div class="row g-3">
          <div class="col-sm-5">
            <h2 class="fs-6 fw-semibold">You need</h2>
            <ul class="small ps-3 mb-0">
              <li>3 leeks, halved</li>
              <li>2 cans white beans</li>
              <li>4 garlic cloves</li>
              <li>500 ml vegetable stock</li>
              <li>1 lemon</li>
            </ul>
          </div>
          <div class="col-sm-7">
            <h2 class="fs-6 fw-semibold">Method</h2>
            <p class="small mb-0">Char the leeks cut side down until blackened, then slice. Soften the garlic, add beans and stock, and simmer for 15 minutes. Fold the leeks through and finish with lemon zest.</p>
          </div>
        </div>
      </div>
      <div class="modal-footer">
        <span class="small me-auto">Serves 4</span>
        <button type="button" class="btn bmd-recipe-btn" data-bs-dismiss="modal"><i class="bi bi-bookmark-heart me-2" aria-hidden="true"></i>Save recipe</button>
      </div>
    </div>
  </div>
</div>

One modal, many triggers

  • show.bs.modal
  • event.relatedTarget
  • data-bs-whatever
  • list-group

Click any Message button.

Each Message button carries data-bs-whatever with a teammate name. The show.bs.modal listener reads it from event.relatedTarget and writes it into the title and the recipient field with textContent and value, so one dialog serves the whole list.

View code
<ul class="list-group bmd-team">
  <li class="list-group-item d-flex align-items-center gap-3"><span class="bmd-avatar bmd-av-1" aria-hidden="true">AK</span><span class="me-auto"><strong class="d-block">Amara Kofi</strong><span class="small text-body-secondary">Design lead</span></span><button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#bmd-msg" data-bs-whatever="Amara Kofi">Message</button></li>
  <li class="list-group-item d-flex align-items-center gap-3"><span class="bmd-avatar bmd-av-2" aria-hidden="true">LV</span><span class="me-auto"><strong class="d-block">Luis Vega</strong><span class="small text-body-secondary">Frontend</span></span><button type="button" class="btn btn-sm btn-outline-primary bmd-msg-luis" data-bs-toggle="modal" data-bs-target="#bmd-msg" data-bs-whatever="Luis Vega">Message</button></li>
  <li class="list-group-item d-flex align-items-center gap-3"><span class="bmd-avatar bmd-av-3" aria-hidden="true">HS</span><span class="me-auto"><strong class="d-block">Hana Sato</strong><span class="small text-body-secondary">Research</span></span><button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#bmd-msg" data-bs-whatever="Hana Sato">Message</button></li>
</ul>
<div class="modal fade" id="bmd-msg" tabindex="-1" aria-labelledby="bmd-msg-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-msg-title">New message</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <form id="bmd-msg-form">
          <div class="mb-3">
            <label for="bmd-msg-to" class="form-label">To</label>
            <input type="text" class="form-control" id="bmd-msg-to" readonly>
          </div>
          <div>
            <label for="bmd-msg-text" class="form-label">Message</label>
            <textarea class="form-control" id="bmd-msg-text" rows="3"></textarea>
          </div>
        </form>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Send</button>
      </div>
    </div>
  </div>
</div>

Toggle between two modals

  • data-bs-toggle="modal"
  • data-bs-target
  • modal swap

Open it, then click Forgot your password.

Bootstrap supports one open modal at a time, but a toggle button inside a modal that targets another modal closes the first and opens the second. The Back link swaps them again, all with data attributes and no script.

View code
<button type="button" class="btn btn-primary bmd-swap-open" data-bs-toggle="modal" data-bs-target="#bmd-swap-a"><i class="bi bi-box-arrow-in-right me-2" aria-hidden="true"></i>Log in</button>
<div class="modal fade" id="bmd-swap-a" tabindex="-1" aria-labelledby="bmd-swap-a-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-swap-a-title">Log in to Quillpad</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <label for="bmd-swap-email" class="form-label">Email</label>
        <input type="email" class="form-control mb-3" id="bmd-swap-email" placeholder="you@example.com" autocomplete="username">
        <label for="bmd-swap-pass" class="form-label">Password</label>
        <input type="password" class="form-control" id="bmd-swap-pass" autocomplete="current-password">
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-link me-auto bmd-swap-forgot" data-bs-target="#bmd-swap-b" data-bs-toggle="modal">Forgot your password?</button>
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Log in</button>
      </div>
    </div>
  </div>
</div>
<div class="modal fade" id="bmd-swap-b" tabindex="-1" aria-labelledby="bmd-swap-b-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-swap-b-title">Reset your password</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <p class="text-body-secondary small">We will send a reset link to the address on your account.</p>
        <label for="bmd-swap-reset" class="form-label">Email</label>
        <input type="email" class="form-control" id="bmd-swap-reset" placeholder="you@example.com" autocomplete="email">
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-link me-auto" data-bs-target="#bmd-swap-a" data-bs-toggle="modal"><i class="bi bi-arrow-left me-1" aria-hidden="true"></i>Back to log in</button>
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Send link</button>
      </div>
    </div>
  </div>
</div>

Live event log with the JS API

  • bootstrap.Modal.getOrCreateInstance
  • show()
  • show.bs.modal
  • shown.bs.modal

Open it, close it and open it again.

The trigger has no data attributes: a click handler calls getOrCreateInstance(...).show(). The modal logs every event with a timestamp, and the "show() returned" line lands between show.bs.modal and shown.bs.modal, which is the async behavior the docs warn about.

View code
<button type="button" class="btn btn-info bmd-log-open"><i class="bi bi-terminal me-2" aria-hidden="true"></i>Open with JavaScript</button>
<div class="modal fade" id="bmd-log" tabindex="-1" aria-labelledby="bmd-log-title" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="bmd-log-title">Modal events</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <p class="small text-body-secondary">Each line shows the milliseconds since the button was clicked.</p>
        <ol class="list-group list-group-numbered font-monospace small bmd-log-list" aria-live="polite"></ol>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary bmd-log-clear">Clear log</button>
        <button type="button" class="btn btn-info" data-bs-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

What is a Bootstrap modal?

Bootstrap’s modal puts content on a layer above the page, dims everything behind it with a backdrop and stops the body from scrolling. Dismiss handling is built in, so a click outside or the Escape key closes it unless you switch that off.

It ships with the Bootstrap framework and opens through data attributes or a JavaScript call, which means no custom overlay code. The official documentation mentions lightboxes and user notifications as typical uses, and says custom content works too.

In general UI work, a modal in UI design blocks the page until the user responds. Bootstrap’s version uses position: fixed, so it sits above everything else in the document. The modal content scrolls instead of the page. A click on the backdrop closes it, unless the backdrop option is set to static.

As of October 2026 the Bootstrap docs list v5.3.8. The August 25, 2025 release post said the plan was for it to be the last patch release before v5.4.0. Bootstrap 6 Alpha 1 followed on October 8, 2026, but it is a pre-release. The maintainers say Bootstrap 5 remains supported and that v5.4.0 will ship before they announce an end-of-life date for it. Everything below describes v5.3.8.

How to create a Bootstrap modal

A working modal needs the Bootstrap CSS and bundle JavaScript loaded on the page, a trigger button, and a div with the modal class that holds the dialog markup. The setup goes like this.

  1. Load the Bootstrap CSS in the head and bootstrap.bundle.min.js before the closing body tag, from jsDelivr or after running npm i bootstrap.
  2. Add a button with data-bs-toggle=”modal” and data-bs-target=”#exampleModal”.
  3. Add a div with the classes modal and fade, the id exampleModal and tabindex=”-1″, placed at the top level of the body.
  4. Nest modal-dialog and modal-content inside it, then the header, body and footer sections.
  5. Add a close button carrying data-bs-dismiss=”modal” to the header or footer.

Required markup structure

The outer container carries the modal class, an id, tabindex=”-1″ and, if you want the animation, fade. Inside it, modal-dialog sets the width and margin, and modal-content is the visible box with the border and background. The header, body and footer live in there. Only modal-body is required (it supplies the padding), and the footer is optional.

The docs ask for a header with a dismiss action, or another explicit dismiss control, whenever possible. They also suggest making modal-title an h1 element sized down with the fs-5 utility, since a dialog is its own document context.

Without Bootstrap’s JavaScript nothing toggles the markup. According to the official JavaScript documentation, the plugins have no fallback when JavaScript is unavailable.

Bootstrap 4 versus Bootstrap 5 attributes

ItemBootstrap 4Bootstrap 5
Open triggerdata-toggle, data-targetdata-bs-toggle, data-bs-target
Dismiss attributedata-dismissdata-bs-dismiss
Close button classclosebtn-close
jQueryRequiredNot required (dropped in 5.0.0)

The migration guide says every plugin’s data attributes are now namespaced with bs. The plugin only listens for the namespaced names, so a Bootstrap 4 snippet pasted into a Bootstrap 5 page leaves the button dead.

How to open and close a Bootstrap modal

Put data-bs-toggle=”modal” and a data-bs-target on a button, or call show() on a modal instance, and the modal opens. Closing works through data-bs-dismiss=”modal” or hide().

Opening with data attributes

Bootstrap’s own docs prefer this method, and it needs no script. Put data-bs-toggle=”modal” on the controller element, then point data-bs-target=”#id” (or href=”#id”) at the modal.

There’s one catch. A single element takes only one set of data attributes, so one button can’t trigger a tooltip and a modal at the same time (Bootstrap JavaScript docs). Put the tooltip on a wrapper element instead.

Opening with JavaScript

The JavaScript route starts with a constructor that accepts an element or a CSS selector. It sits in Bootstrap’s programmatic API next to the static lookup methods.

  • new bootstrap.Modal(‘#myModal’, options) creates an instance.
  • getOrCreateInstance returns the existing instance or builds one, so repeated clicks reuse a single object.
  • getInstance returns null when the element was never initialized.
  • show(), hide() and toggle() open, close and flip the modal.

To open a modal on page load, create the instance and call show() once the DOM is ready.

None of this needs jQuery in Bootstrap 5, though when jQuery is present on the page Bootstrap also registers its plugins on it (official docs).

Closing a modal

Where you put the close control matters, and only one placement is recommended.

Inside the modal, a button with data-bs-dismiss=”modal” does the job. Most setups have the header close button plus a Close button in the footer.

Outside the modal, the same attribute works if you add a data-bs-target pointing at the modal id. The docs warn that this does not match the ARIA Authoring Practices Guide dialog pattern and say to use it at your own risk.

A click on the backdrop and the Escape key also close a modal by default. The backdrop and keyboard options switch both behaviors off.

Bootstrap modal sizes and layout classes

Width comes from a size class on the modal-dialog element. These sizes only apply from certain breakpoints up, which prevents horizontal scrollbars on narrow viewports (Bootstrap docs, v5.3).

The figures from the v5.3 docs:

  • modal-sm caps the width at 300px.
  • With no class you get the default 500px.
  • modal-lg gives 800px.
  • modal-xl goes up to 1140px.
  • The fullscreen-below-a-breakpoint classes, modal-fullscreen-sm-down to modal-fullscreen-xxl-down, cover breakpoints from 576px to 1400px.

Fullscreen and responsive fullscreen

modal-fullscreen covers the whole viewport at every width.

The -down variants apply only below a breakpoint. modal-fullscreen-md-down goes fullscreen under 768px and stays a regular dialog above it, which is plain responsive design applied to a dialog.

For forms I’d make modal-fullscreen-md-down the default, since phones then get the full screen for inputs.

Centered and scrollable modals

modal-dialog-centered centers the dialog vertically. modal-dialog-scrollable keeps the header and footer in place and scrolls only the modal body once content outgrows the viewport. Leave it off and the whole modal scrolls over the page instead. Add both classes for a centered modal with a scrolling body.

Using the grid inside a modal

The docs give one rule for layouts inside a modal. Nest a container-fluid inside modal-body, then use the normal grid system classes.

Grid breakpoints follow the viewport width, not the modal width. A col-md-4 column inside a 500px modal still switches layout at a 768px viewport, which surprises people the first time.

Which options, methods and events control a Bootstrap modal?

The options are few. Bootstrap exposes seven methods in all and the modal fires five events, so most of the scripting weight sits in those last two.

Options and defaults

The three options are backdrop, keyboard and focus.

OptionAcceptsDefaultEffect
backdroptrue, false, “static”trueAdds a backdrop. Static blocks closing on outside click
keyboardbooleantrueEscape closes the modal
focusbooleantrueMoves focus to the modal on initialization

Options pass through data attributes in kebab-case (data-bs-backdrop=”static”) or through a JavaScript object. When both are set, the JavaScript object wins.

Since Bootstrap 5.2.0 an experimental data-bs-config attribute accepts the same settings as JSON. Defaults change globally with bootstrap.Modal.Default.keyboard = false (official JavaScript docs).

Static backdrop and preventing close

Set backdrop to static and keyboard to false, and the modal ignores outside clicks and the Escape key. In markup that means data-bs-backdrop=”static” and data-bs-keyboard=”false” on the modal div.

Each blocked click or Escape press fires hidePrevented.bs.modal. From script, hide.bs.modal can be cancelled with preventDefault(), which blocks any close, including one triggered by hide().

Static suits forms with unsaved input. I’d skip it for plain information, where users expect a click outside to dismiss the modal.

Methods and the async caveat

Every modal method is asynchronous. It returns as soon as the transition starts, before shown.bs.modal or hidden.bs.modal fires (Bootstrap docs).

A call made while the modal is mid-transition is ignored. Bootstrap’s Sass default for the modal transition is transform .3s ease-out (v5.3.8), short enough to miss in testing, but a call inside it is dropped.

Beyond the ones covered above, handleUpdate() readjusts the position when the modal height changes while it’s open, for example when a scrollbar appears. dispose() removes the stored data on the element. Calling dispose() right after hide() gives incorrect results, so wait for hidden.bs.modal and then dispose.

Events and firing order

A normal open and close fires show, shown, hide and hidden, in that order. All modal events fire on the modal element itself.

  • show.bs.modal fires immediately when show is called, before the transition.
  • shown.bs.modal waits until the modal is visible and the transition has finished.
  • hide.bs.modal fires immediately on hide and can be cancelled.
  • hidden.bs.modal comes after the modal is fully hidden.

hidePrevented.bs.modal sits outside that sequence. It fires when a static backdrop or keyboard: false blocks a close.

Loading different content into one modal

One modal can serve many buttons. Give each button a data-bs-* attribute (the docs use data-bs-whatever), read event.relatedTarget inside a show.bs.modal listener, and write the value into the modal title or a form field.

Because show.bs.modal fires before the transition, the swap happens out of sight. For remote data, start an Ajax request in the same listener and fill the modal when the response arrives.

relatedTarget is set when a click opens the modal. Calling show(trigger) from JavaScript passes the same element along.

How to style and animate a Bootstrap modal

Override the CSS variables set on .modal and .modal-backdrop, or edit the Sass variables before compiling. Animation runs through the fade class and two transform variables.

The CSS variables, added in v5.2.0, are scoped to the component, so one modal can differ from the rest.

  • –bs-modal-width, –bs-modal-bg, –bs-modal-color
  • –bs-modal-border-radius and –bs-modal-box-shadow
  • –bs-modal-header-padding-x and –bs-modal-header-padding-y
  • –bs-backdrop-bg and –bs-backdrop-opacity, which live on the backdrop

The values come from Sass, so a Sass build keeps working. A project-wide change belongs in Sass, and a one-off change goes on the element.

The backdrop defaults to black at an opacity of .5 ($modal-backdrop-bg and $modal-backdrop-opacity). Raising –bs-backdrop-opacity on a single modal is the quickest way to separate it from busy content.

The shadow is one variable per modal, and a CSS box shadow generator produces the value to paste in.

On the animation side, $modal-fade-transform sets the dialog’s start state and $modal-show-transform sets the end state. The default start is translate(0, -50px), a slide down from above, and $modal-fade-transform: scale(.8) turns it into a zoom-in. Remove the fade class and the modal appears with no transition at all.

Per the Bootstrap accessibility docs, most transition effects, including modal open and close, switch off for users who set prefers-reduced-motion. Animations added through your own keyframes sit outside that rule, so wrap them in a media query for the same preference.

Bootstrap 5.3.0 added color modes. Set data-bs-theme=”dark” on the modal element and the dialog follows the dark palette. The btn-close-white class is deprecated for the same job, so put the attribute on the close button or a parent instead (Bootstrap migration guide).

How to make a Bootstrap modal accessible

Bootstrap adds role=”dialog” in JavaScript, so you never write it by hand. What’s left is naming the dialog with aria-labelledby, pointing at its title, and placing focus deliberately when the modal opens.

The ARIA attributes that name and describe the dialog are in the table.

AttributeGoes onJob
aria-labelledbyModal containerPoints to the visible title
aria-describedbyModal containerOptional short description
aria-label=”Close”Close buttonNames the icon-only button

The W3C ARIA Authoring Practices Guide adds one rule. Skip aria-describedby when the content holds lists, tables or several paragraphs, because a screen reader reads that description as one unbroken string.

On focus, the W3C dialog pattern says the first focusable element is right for most dialogs. When the content is long or structured, a static element with tabindex=”-1″ at the top, such as the title, works better. And if the dialog confirms a delete or a payment, focus goes to the least destructive button.

The HTML autofocus attribute does nothing inside a Bootstrap modal (official docs). Focus the chosen element from a shown.bs.modal listener instead, which for a delete confirmation means the Cancel button.

The same W3C pattern requires Tab and Shift+Tab to cycle inside the dialog and focus to return to the invoking element on close. Bootstrap’s docs say its components are designed to work for keyboard and mouse users as well as touch, but overall web accessibility still comes down to the author’s markup and scripts. Test both behaviors by hand.

One reported failure is GitHub issue twbs/bootstrap #39862. It describes a full Tab cycle inside an open modal scrolling the page to wherever the modal markup sits in the DOM, including on Bootstrap’s own 5.3 demo. Maintainers confirmed it in July 2024. The issue was closed on June 23, 2026, with the explanation that the cause was Bootstrap’s scripted focus trap and that the v6 development branch relies on the native dialog element’s focus handling instead. The issue page links no fix for v5, so test the Tab cycle with the markup in its final position.

When does a Bootstrap modal stop working?

The documented trouble spots are a second modal opening on top of the first, markup sitting inside another fixed element, a Bootstrap version that differs from the one the code was written for, and mobile browsers scrolling the page behind the dialog.

Nested modals

Bootstrap supports one modal window at a time. The maintainers consider nested modals a poor user experience, so they’re unsupported.

What is supported is a swap. Put data-bs-toggle=”modal” and data-bs-target=”#secondModal” on a button inside the first modal. The first modal closes as the second opens, and a Back button carrying the first modal’s id swaps them again. Two modals are never open together.

Backdrop and z-index problems

  • The page is dimmed and the modal won’t respond to clicks. That usually means the modal sits inside another fixed element, which the docs flag as a source of rendering problems.
  • A navbar or custom element stays above the backdrop when its z-index is higher than the modal scale.
  • A toast showing over an open modal is expected, since toasts sit at 1090 and the modal at 1055 in v5.3.8.

Move the modal markup out of the fixed container before touching any z-index value.

Version differences

Bootstrap docs versionBackdrop z-indexModal z-index
4.610401050
5.0.210401060
5.3.810501055

Code pinned to one value breaks on another. The ng-bootstrap issue #4139 reports a stacked-modal backdrop fixed at 1050 that stopped working when Bootstrap 5 moved the modal to 1060.

Bootstrap’s docs say that changing one value in the scale likely means changing all of them. If an override is unavoidable, follow a clean approach to overriding Bootstrap CSS and adjust the whole scale.

Internet Explorer is a dead end. The Bootstrap 5 docs say IE is not supported and send IE users to Bootstrap v4, but Bootstrap 4 reached end of life on January 1, 2023 (Bootstrap v4.6.2 release post). A project that needs IE has no supported modal to use.

Mobile scrolling quirks

Support for overflow: hidden on the body is limited on iOS and Android. Scrolling past the top or bottom of a modal can scroll the page underneath, and a gesture that starts inside an input or textarea scrolls the body instead of the modal (Bootstrap browsers and devices docs).

The docs still carry both notes at v5.3.8, and the iOS entry is dated to iOS 9.2. Run a cross-browser compatibility check on current devices before designing around either one.

Bootstrap modal vs offcanvas, toast and the native dialog element

Reach for the Bootstrap modal when the user has to answer before continuing. Side panels belong in an offcanvas, and notices that need no reply belong in a toast. If you want a modal without Bootstrap JavaScript, the native dialog element does that.

ComponentBlocks the pageBest use
Bootstrap modalYes, backdrop and body scroll removedConfirmations, short forms
OffcanvasYes by default, backdrop and scroll are optionsNavigation, carts
ToastNoBrief notices
Native dialog (showModal)Yes, rest of page inertModal with no framework

An offcanvas is a sidebar that slides in from any viewport edge, the same idea as a drawer in UI design. It shares code with the modal, and only one can show at a time.

Its defaults are 400px wide for left and right placement, 30vh tall for top and bottom, and body scrolling off while it is open (Bootstrap docs, v5.3).

A toast notification blocks nothing, hides itself after 5000 ms by default and must be initialized in script.

I’d pick a toast over a modal for confirmations that need no decision, since modals interrupt and toasts don’t. Controls inside an autohiding toast are hard for keyboard users to reach in time, so anything actionable belongs in a modal or an alert.

On the native side, showModal() opens a modal dialog, makes everything outside it inert, closes on Escape and styles its backdrop through ::backdrop. MDN lists it as Baseline widely available, supported across browsers since March 2022.

It ships no size classes, grid or Sass variables, and animating it takes @starting-style plus allow-discrete transitions (MDN).

On a page that already loads Bootstrap, the Bootstrap modal is the shorter path, because sizes, scrollable bodies and CSS variables come ready. On a page without Bootstrap, the native element saves you from shipping a framework for one dialog.

Bootstrap Modal FAQ

How do you add a form to a modal?

Put the form inside modal-body and give it an id. The footer sits outside that element, so a footer button needs form=”yourFormId” with type=”submit” to submit it.

Bootstrap’s own varying-content demo uses this layout, with the fields in the body and the buttons in the footer.

Why does a YouTube video keep playing after the modal closes?

Bootstrap does not control embedded players, and its docs say stopping playback takes extra JavaScript.

Listen for hidden.bs.modal, then reset the iframe src or call the player’s pause method.

Do tooltips and popovers work inside a modal?

Yes. The docs allow both inside modals and dismiss them automatically when the modal closes.

They still depend on Popper, which bootstrap.bundle.min.js already includes. With bootstrap.min.js, load Popper separately.

Can you use the modal JavaScript in React, Vue or Angular?

Not reliably. Bootstrap’s docs say its JavaScript is not fully compatible with those frameworks, because both can end up mutating the same DOM element and cause bugs such as dropdowns stuck open.

A framework-specific package is the better route. That means React Bootstrap for React, ng-bootstrap or ngx-bootstrap for Angular, and BootstrapVueNext for Vue 3 (alpha in the v5.3.8 docs).

Checks to Run Before a Bootstrap Modal Ships

Before release, a modal needs four checks: the Bootstrap version in production, the markup’s final DOM position, a keyboard-only pass, and a test on a real phone. The order matters.

  • Version goes first, since a wrong attribute prefix blocks opening.
  • DOM position comes next, because stacking can hide the dialog.
  • The keyboard and phone passes go last, as both need an open modal.

Each check takes minutes, and only the phone pass needs hardware. Folding them into a full web accessibility checklist keeps the keyboard pass from getting skipped under deadline.

These checks were verified against the Bootstrap docs at v5.3.8 on October 28, 2026, and any minor or major upgrade reopens the first one.