Source Code

<div class="container py-5">
  <div class="row bst-shell mx-auto">
    <div class="col-3">
      <div class="nav flex-column nav-pills" id="bstTabs" role="tablist" aria-orientation="vertical">
        <button class="nav-link active text-start" data-bs-toggle="pill" data-bs-target="#bstProfile" type="button" role="tab">Profile</button>
        <button class="nav-link text-start" data-bs-toggle="pill" data-bs-target="#bstSecurity" type="button" role="tab">Security</button>
        <button class="nav-link text-start" data-bs-toggle="pill" data-bs-target="#bstNotifications" type="button" role="tab">Notifications</button>
        <button class="nav-link text-start" data-bs-toggle="pill" data-bs-target="#bstBilling" type="button" role="tab">Billing</button>
      </div>
    </div>

    <div class="col-9">
      <div class="tab-content card bst-card">
        <div class="card-body p-4">
          <div class="tab-pane fade show active" id="bstProfile" role="tabpanel">
            <h5 class="fw-bold mb-3">Profile</h5>
            <div class="mb-3">
              <label class="form-label small">Display name</label>
              <input type="text" class="form-control" value="Jordan Blake">
            </div>
            <div class="mb-2">
              <label class="form-label small d-flex justify-content-between">
                <span>Bio</span>
                <span class="text-muted" id="bstBioCount">0 / 160</span>
              </label>
              <textarea class="form-control" id="bstBio" rows="3" maxlength="160" placeholder="Tell us about yourself..."></textarea>
            </div>
          </div>

          <div class="tab-pane fade" id="bstSecurity" role="tabpanel">
            <h5 class="fw-bold mb-3">Security</h5>
            <div class="mb-3">
              <label class="form-label small">Current password</label>
              <input type="password" class="form-control" placeholder="••••••••">
            </div>
            <div class="mb-3">
              <label class="form-label small">New password</label>
              <input type="password" class="form-control" placeholder="New password">
            </div>
            <button class="btn btn-dark btn-sm">Update password</button>
          </div>

          <div class="tab-pane fade" id="bstNotifications" role="tabpanel">
            <h5 class="fw-bold mb-3">Notifications</h5>
            <div class="form-check form-switch mb-3">
              <input class="form-check-input" type="checkbox" role="switch" id="bstNotifyEmail" checked>
              <label class="form-check-label" for="bstNotifyEmail">Email notifications</label>
            </div>
            <div class="form-check form-switch mb-3">
              <input class="form-check-input" type="checkbox" role="switch" id="bstNotifySms">
              <label class="form-check-label" for="bstNotifySms">SMS alerts</label>
            </div>
            <div class="form-check form-switch mb-3">
              <input class="form-check-input" type="checkbox" role="switch" id="bstNotifyMarketing" checked>
              <label class="form-check-label" for="bstNotifyMarketing">Marketing emails</label>
            </div>
            <p class="small text-muted mb-0" id="bstNotifySummary"></p>
          </div>

          <div class="tab-pane fade" id="bstBilling" role="tabpanel">
            <h5 class="fw-bold mb-3">Billing</h5>
            <p class="text-muted small">Current plan: <strong>Pro — $29/mo</strong></p>
            <button class="btn btn-outline-dark btn-sm">Manage subscription</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Bootstrap Settings Page with Tabs — Free Snippet

Bootstrap Settings Page with Tabs · Dashboards · Plain HTML, CSS & JS · Live preview

What's included

Features

Real Bootstrap 5.3 vertical nav-pills wired to tab-content panes via genuine data-bs-toggle="pill"
Tab switching, fade transitions, and ARIA roles handled entirely by Bootstrap's own Tab plugin
Bio textarea enforces a hard 160-character limit via a native maxlength attribute
Live character counter updates on every keystroke via a plain input event listener
Three real Bootstrap form-switch toggles control notification preferences
A shared updateSummary() function derives a readable sentence from the current switch states
Explicit "all off" message handles the edge case where no notification types are enabled
Four distinct panes (Profile, Security, Notifications, Billing) covering a realistic settings surface

About this UI Snippet

Bootstrap Settings Page with Tabs — HTML, CSS & JavaScript

Screenshot of the Bootstrap Settings Page with Tabs snippet rendered live

Settings pages live or die on whether the tab switching actually feels native, and this snippet uses Bootstrap's own tab machinery rather than hand-rolled show/hide logic — every pill button carries data-bs-toggle="pill" and a data-bs-target pointing at its matching .tab-pane, and Bootstrap's bundled JS (bootstrap.bundle.min.js, which includes Popper and the Tab plugin) handles the fade show active class choreography, ARIA role="tab"/role="tabpanel" wiring, and keyboard behavior automatically. The nav itself is a real nav flex-column nav-pills with aria-orientation="vertical", styled with a small additive .bst-card/#bstTabs .nav-link.active layer on top rather than a custom-built tab system.

The two pieces of actual custom JavaScript live inside two of the four panes. In Profile, a <textarea> with a real maxlength="160" attribute (so the browser itself enforces the hard limit) is paired with a live counter: an input event listener reads bio.value.length on every keystroke and writes "N / 160" into #bstBioCount, giving the user constant feedback on how much room is left without ever letting the count exceed the MAX_BIO constant, since the native maxlength attribute stops further typing at exactly 160 characters regardless of what the counter displays.

In Notifications, three real Bootstrap form-check form-switch toggles (Email, SMS, Marketing) each fire a change event that calls a shared updateSummary() function. That function checks each switch's .checked state, pushes a matching label into an on array, and joins it into a sentence like "You will receive: email, marketing." — or, when every switch is off, an explicit "All notifications are turned off" message rather than an empty or broken-looking string, which is the small edge case that a naive on.join(', ') alone would render as just a bare period.

The Security pane demonstrates plain password fields and an update button with no extra JS wired (deliberately left as a static form, since password changes belong behind a real backend call), and the Billing pane shows a static current-plan summary — both included to round out a realistic four-tab settings page rather than over-engineering panes that do not need dynamic behavior.

Because the tab switching is handled entirely by Bootstrap's own JS plugin rather than custom code, there is nothing to reimplement when porting to React, Vue, or Angular beyond re-initializing the Bootstrap Tab/Pill component (or using a framework-native tabs pattern instead); the bio counter and notification summary, being plain event listeners over a fixed set of ids, drop straight into a useEffect/onMounted/ngAfterViewInit block with refs replacing the getElementById calls.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI coding assistant like Claude to add a "Save changes" sticky footer that only enables once a field has been edited, or to persist the active tab in the URL hash so refreshing the page keeps the same tab open. It's also worth asking it to add form validation to the password fields.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Bootstrap 5.3 settings page using the real Bootstrap CDN (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble Bootstrap.

Requirements:
- A vertical Bootstrap nav-pills sidebar (Profile, Security, Notifications, Billing) wired to tab-content panes using real data-bs-toggle="pill" and data-bs-target attributes, relying on Bootstrap's own Tab plugin for switching, not custom JS.
- The Profile pane needs a bio textarea with a native maxlength of 160 characters and a live counter showing "current length / 160" that updates on every keystroke.
- The Notifications pane needs at least three Bootstrap form-switch toggles, with a summary sentence below them that updates live to list which notification types are currently enabled, and shows an explicit "all off" message when none are.
- Security and Billing panes should show realistic static content for those sections.
- All tab, textarea, and switch behavior should work with plain vanilla JavaScript beyond Bootstrap's own bundled plugin.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the snippetThe Profile tab is active and highlighted dark in the vertical pill nav, showing a display-name field and an empty bio textarea with a "0 / 160" counter.
  2. 2
    Type in the bio textareaThe counter next to "Bio" updates on every keystroke, and typing stops accepting new characters once you reach 160.
  3. 3
    Click the Security tabThe pane swaps with Bootstrap's fade transition to show password fields, and the Security pill becomes the highlighted active tab.
  4. 4
    Click the Notifications tab and toggle a switchThe summary sentence below the three switches updates immediately to reflect exactly which notification types are currently enabled.
  5. 5
    Turn all three notification switches offThe summary changes to "All notifications are turned off" instead of showing an empty or malformed sentence.

Real-world uses

Common Use Cases

DASHBOARD
Account and app settings pages
A complete settings shell ready to extend, similar in structure to an admin dashboard sidebar navigation pattern.
User profile management
Reuse the character-limited bio field pattern anywhere a user edits a short public description, such as beside a team member grid profile.
Learning Bootstrap's native tab plugin
A clean example of using data-bs-toggle="pill" correctly instead of writing custom tab-switching JavaScript.
SaaS account preference centers
The notification-switch summary pattern generalizes well to any app exposing multiple opt-in channels to a user.
CART
Billing and subscription management
Extend the Billing pane with real plan data, pairing naturally with the subscription plans cards snippet for an upgrade flow.

Got questions?

Frequently Asked Questions

Each pill button has data-bs-toggle="pill" and a data-bs-target matching a .tab-pane id; Bootstrap's bundled JavaScript (which includes the Tab plugin) listens for clicks on those attributes and handles adding/removing the fade, show, and active classes and ARIA state — no custom tab-switching code was written.

No — the textarea has a native HTML maxlength="160" attribute, so the browser itself prevents typing or pasting beyond that limit; the JavaScript counter is purely a readout of the current length, not the enforcement mechanism.

It explicitly reads "All notifications are turned off" — the updateSummary() function checks for this case and returns that fixed message instead of joining an empty array, which would otherwise produce a blank or malformed sentence.

Yes — for the tabs, either keep Bootstrap's JS and initialize it in useEffect/onMounted/ngAfterViewInit, or replace it with your framework's native tab state; for the bio counter and notification summary, move the input/change listeners into the same lifecycle hooks using refs instead of getElementById, and clean up listeners on unmount if you attach them manually.

The nav-pills, tab-content, and form-switch classes are visual — you can restyle them with Tailwind, but you would need to either keep Bootstrap's JS loaded just for the Tab plugin's behavior or reimplement pane switching yourself, since Tailwind has no equivalent JS component.

No — the Security pane is intentionally left as static markup with no JavaScript wired to the Update password button, since a real implementation needs a server-side call; wire your own fetch/axios request to that button's click event.