28 CSS Hamburger Menus16 / 28

Bootstrap 5MIT licensed

Bootstrap 5 Navbar Hamburger Menu Toggle Setup

The canonical Bootstrap 5.3 navbar, assembled correctly on the first try: navbar-expand-lg picks the breakpoint, navbar-toggler + data-bs-toggle wire the collapse plugin with zero custom JS, and every class in the chain is annotated so you know which ones are load-bearing. Includes the three fixes for 'bootstrap hamburger not working' (the #1 support thread for this pattern) and the CSS to replace the default toggler icon with an animated one.

Published Updated

Live Demo
Try it

The code

<section class="chm-16" aria-label="Bootstrap 5 navbar hamburger setup demo">
  <div class="chm-16__stage">
    <div class="chm-16__frame">
      <nav class="navbar" aria-label="Main">
        <div class="container-fluid">
          <a class="navbar-brand" href="#home"><span class="brand-dot" aria-hidden="true"></span>Bootlace</a>
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#chm16-nav" aria-controls="chm16-nav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="chm-16__bars" aria-hidden="true"><i></i><i></i><i></i></span>
          </button>
          <div class="collapse navbar-collapse" id="chm16-nav">
            <ul class="navbar-nav">
              <li class="nav-item"><a class="nav-link active" aria-current="page" href="#home">Home</a></li>
              <li class="nav-item"><a class="nav-link" href="#features">Features</a></li>
              <li class="nav-item"><a class="nav-link" href="#pricing">Pricing</a></li>
              <li class="nav-item"><a class="nav-link disabled" aria-disabled="true">Beta</a></li>
            </ul>
          </div>
        </div>
      </nav>
      <div class="chm-16__page" aria-hidden="true"><span style="--w:76%"></span><span style="--w:90%"></span><span style="--w:58%"></span></div>
    </div>
    <p class="chm-16__note" aria-hidden="true">card is narrower than the <code>lg</code> breakpoint, so the toggler shows. The shim stands in for <code>bootstrap.bundle.min.js</code> here — load the real bundle in your project.</p>
  </div>
</section>
.chm-16,
.chm-16 *,
.chm-16 *::before,
.chm-16 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-16 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --body: oklch(1 0 0);
  --tertiary: oklch(0.97 0.004 260);
  --border: oklch(0.9 0.007 260);
  --ink: oklch(0.28 0.02 262);
  --mut: oklch(0.5 0.016 262);
  --primary: oklch(0.55 0.19 262);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.chm-16__stage {
  border: 1px solid var(--border);
  border-radius: 18px;
  background: oklch(0.985 0.003 260);
  padding: 20px;
  display: grid;
  gap: 14px;
}

.chm-16__frame {
  container-type: inline-size;
  border: 1px solid var(--border);
  border-radius: 14px;
  overflow: hidden;
  background: var(--body);
  box-shadow: 0 14px 34px -26px oklch(0.3 0.03 262/.6);
}

.chm-16 .navbar {
  background: var(--tertiary);
  border-bottom: 1px solid var(--border);
  padding: 8px 0;
}

.chm-16 .container-fluid {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 0 16px;
}

.chm-16 .navbar-brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 16.5px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  padding: 6px 0;
}

.chm-16 .brand-dot {
  width: 19px;
  height: 19px;
  border-radius: 5px;
  background: linear-gradient(140deg,var(--primary),oklch(0.65 0.2 300));
}

.chm-16 .navbar-toggler {
  display: grid;
  place-items: center;
  width: 52px;
  height: 44px;
  border: 1px solid oklch(0.82 0.012 262);
  border-radius: 8px;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: box-shadow .15s;
}

.chm-16 .navbar-toggler:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklch,var(--primary) 35%,transparent);
}

.chm-16__bars {
  position: relative;
  width: 20px;
  height: 14px;
  display: block;
}

.chm-16__bars i {
  position: absolute;
  left: 0;
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: translate .28s,rotate .28s .04s,opacity .2s;
}

.chm-16__bars i:nth-child(1) {
  top: 0;
}

.chm-16__bars i:nth-child(2) {
  top: 6px;
}

.chm-16__bars i:nth-child(3) {
  top: 12px;
}

.chm-16 .navbar-toggler[aria-expanded="true"] .chm-16__bars i:nth-child(1) {
  translate: 0 6px;
  rotate: 45deg;
}

.chm-16 .navbar-toggler[aria-expanded="true"] .chm-16__bars i:nth-child(2) {
  opacity: 0;
}

.chm-16 .navbar-toggler[aria-expanded="true"] .chm-16__bars i:nth-child(3) {
  translate: 0 -6px;
  rotate: -45deg;
}

.chm-16 .navbar-collapse {
  flex-basis: 100%;
  overflow: hidden;
}

.chm-16 .navbar-collapse.collapse:not(.show) {
  display: none;
}

.chm-16 .navbar-collapse.collapsing {
  height: 0;
  transition: height .3s ease;
}

.chm-16 .navbar-nav {
  list-style: none;
  display: flex;
  flex-direction: column;
  padding: 6px 4px 10px;
}

.chm-16 .nav-link {
  display: block;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 14.5px;
  font-weight: 500;
  color: var(--mut);
  text-decoration: none;
  transition: color .15s,background .15s;
}

.chm-16 .nav-link:hover,
.chm-16 .nav-link:focus-visible {
  color: var(--ink);
  background: oklch(0.94 0.006 260);
}

.chm-16 .nav-link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklch,var(--primary) 35%,transparent);
}

.chm-16 .nav-link.active {
  color: var(--ink);
  font-weight: 600;
}

.chm-16 .nav-link.disabled {
  color: oklch(0.72 0.008 262);
  pointer-events: none;
}

@container (min-width: 768px) {
  .chm-16 .navbar-toggler {
    display: none;
  }

  .chm-16 .navbar-collapse {
    flex-basis: auto;
  }

  .chm-16 .navbar-collapse.collapse:not(.show) {
    display: flex !important;
  }

  .chm-16 .navbar-nav {
    flex-direction: row;
    gap: 2px;
    padding: 0;
    margin-left: auto;
  }
}

.chm-16__page {
  padding: 16px;
  display: grid;
  gap: 11px;
}

.chm-16__page span {
  display: block;
  height: 10px;
  width: var(--w);
  border-radius: 99px;
  background: oklch(0.94 0.004 260);
}

.chm-16__note {
  font-size: 12px;
  line-height: 1.6;
  color: var(--mut);
  text-wrap: pretty;
}

.chm-16__note code {
  font: 11px ui-monospace,Menlo,Consolas,monospace;
  background: oklch(0.93 0.005 260);
  border-radius: 5px;
  padding: 1.5px 6px;
}

@media (prefers-reduced-motion: reduce) {
  .chm-16 * {
    transition-duration: .01s !important;
  }
}
(function () {
  /* Stand-in for bootstrap.bundle.min.js collapse — same class dance, same timing. */
  document.querySelectorAll('.chm-16__frame').forEach(function (frame) {
    if (frame.dataset.bound) return; frame.dataset.bound = '1';
    var btn = frame.querySelector('[data-bs-toggle="collapse"]'),
        panel = frame.querySelector(btn.getAttribute('data-bs-target'));
    btn.addEventListener('click', function () {
      var opening = !panel.classList.contains('show');
      btn.setAttribute('aria-expanded', String(opening));
      panel.classList.remove('collapse');
      panel.classList.add('collapsing');
      panel.style.height = opening ? '0' : panel.scrollHeight + 'px';
      requestAnimationFrame(function () {
        panel.style.height = opening ? panel.scrollHeight + 'px' : '0';
      });
      setTimeout(function () {
        panel.classList.remove('collapsing');
        panel.classList.add('collapse');
        panel.classList.toggle('show', opening);
        panel.style.height = '';
      }, 300);
    });
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Hamburger Menu from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Bootstrap 5 Navbar Hamburger Menu Toggle Setup
Source: https://codefronts.com/navigation/css-hamburger-menus/bootstrap-5-responsive-navbar-with-hamburger/

The canonical Bootstrap 5.3 navbar, assembled correctly on the first try: navbar-expand-lg picks the breakpoint, navbar-toggler + data-bs-toggle wire the collapse plugin with zero custom JS, and every class in the chain is annotated so you know which ones are load-bearing. Includes the three fixes for 'bootstrap hamburger not working' (the #1 support thread for this pattern) and the CSS to replace the default toggler icon with an animated one.
## HTML
```html
<section class="chm-16" aria-label="Bootstrap 5 navbar hamburger setup demo">
  <div class="chm-16__stage">
    <div class="chm-16__frame">
      <nav class="navbar" aria-label="Main">
        <div class="container-fluid">
          <a class="navbar-brand" href="#home"><span class="brand-dot" aria-hidden="true"></span>Bootlace</a>
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#chm16-nav" aria-controls="chm16-nav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="chm-16__bars" aria-hidden="true"><i></i><i></i><i></i></span>
          </button>
          <div class="collapse navbar-collapse" id="chm16-nav">
            <ul class="navbar-nav">
              <li class="nav-item"><a class="nav-link active" aria-current="page" href="#home">Home</a></li>
              <li class="nav-item"><a class="nav-link" href="#features">Features</a></li>
              <li class="nav-item"><a class="nav-link" href="#pricing">Pricing</a></li>
              <li class="nav-item"><a class="nav-link disabled" aria-disabled="true">Beta</a></li>
            </ul>
          </div>
        </div>
      </nav>
      <div class="chm-16__page" aria-hidden="true"><span style="--w:76%"></span><span style="--w:90%"></span><span style="--w:58%"></span></div>
    </div>
    <p class="chm-16__note" aria-hidden="true">card is narrower than the <code>lg</code> breakpoint, so the toggler shows. The shim stands in for <code>bootstrap.bundle.min.js</code> here — load the real bundle in your project.</p>
  </div>
</section>
```
## CSS
```css
.chm-16,
.chm-16 *,
.chm-16 *::before,
.chm-16 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.chm-16 {
  background: var(--bg);
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  --body: oklch(1 0 0);
  --tertiary: oklch(0.97 0.004 260);
  --border: oklch(0.9 0.007 260);
  --ink: oklch(0.28 0.02 262);
  --mut: oklch(0.5 0.016 262);
  --primary: oklch(0.55 0.19 262);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.chm-16__stage {
  border: 1px solid var(--border);
  border-radius: 18px;
  background: oklch(0.985 0.003 260);
  padding: 20px;
  display: grid;
  gap: 14px;
}

.chm-16__frame {
  container-type: inline-size;
  border: 1px solid var(--border);
  border-radius: 14px;
  overflow: hidden;
  background: var(--body);
  box-shadow: 0 14px 34px -26px oklch(0.3 0.03 262/.6);
}

.chm-16 .navbar {
  background: var(--tertiary);
  border-bottom: 1px solid var(--border);
  padding: 8px 0;
}

.chm-16 .container-fluid {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 0 16px;
}

.chm-16 .navbar-brand {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 16.5px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  padding: 6px 0;
}

.chm-16 .brand-dot {
  width: 19px;
  height: 19px;
  border-radius: 5px;
  background: linear-gradient(140deg,var(--primary),oklch(0.65 0.2 300));
}

.chm-16 .navbar-toggler {
  display: grid;
  place-items: center;
  width: 52px;
  height: 44px;
  border: 1px solid oklch(0.82 0.012 262);
  border-radius: 8px;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: box-shadow .15s;
}

.chm-16 .navbar-toggler:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklch,var(--primary) 35%,transparent);
}

.chm-16__bars {
  position: relative;
  width: 20px;
  height: 14px;
  display: block;
}

.chm-16__bars i {
  position: absolute;
  left: 0;
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: translate .28s,rotate .28s .04s,opacity .2s;
}

.chm-16__bars i:nth-child(1) {
  top: 0;
}

.chm-16__bars i:nth-child(2) {
  top: 6px;
}

.chm-16__bars i:nth-child(3) {
  top: 12px;
}

.chm-16 .navbar-toggler[aria-expanded="true"] .chm-16__bars i:nth-child(1) {
  translate: 0 6px;
  rotate: 45deg;
}

.chm-16 .navbar-toggler[aria-expanded="true"] .chm-16__bars i:nth-child(2) {
  opacity: 0;
}

.chm-16 .navbar-toggler[aria-expanded="true"] .chm-16__bars i:nth-child(3) {
  translate: 0 -6px;
  rotate: -45deg;
}

.chm-16 .navbar-collapse {
  flex-basis: 100%;
  overflow: hidden;
}

.chm-16 .navbar-collapse.collapse:not(.show) {
  display: none;
}

.chm-16 .navbar-collapse.collapsing {
  height: 0;
  transition: height .3s ease;
}

.chm-16 .navbar-nav {
  list-style: none;
  display: flex;
  flex-direction: column;
  padding: 6px 4px 10px;
}

.chm-16 .nav-link {
  display: block;
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 14.5px;
  font-weight: 500;
  color: var(--mut);
  text-decoration: none;
  transition: color .15s,background .15s;
}

.chm-16 .nav-link:hover,
.chm-16 .nav-link:focus-visible {
  color: var(--ink);
  background: oklch(0.94 0.006 260);
}

.chm-16 .nav-link:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px color-mix(in oklch,var(--primary) 35%,transparent);
}

.chm-16 .nav-link.active {
  color: var(--ink);
  font-weight: 600;
}

.chm-16 .nav-link.disabled {
  color: oklch(0.72 0.008 262);
  pointer-events: none;
}

@container (min-width: 768px) {
  .chm-16 .navbar-toggler {
    display: none;
  }

  .chm-16 .navbar-collapse {
    flex-basis: auto;
  }

  .chm-16 .navbar-collapse.collapse:not(.show) {
    display: flex !important;
  }

  .chm-16 .navbar-nav {
    flex-direction: row;
    gap: 2px;
    padding: 0;
    margin-left: auto;
  }
}

.chm-16__page {
  padding: 16px;
  display: grid;
  gap: 11px;
}

.chm-16__page span {
  display: block;
  height: 10px;
  width: var(--w);
  border-radius: 99px;
  background: oklch(0.94 0.004 260);
}

.chm-16__note {
  font-size: 12px;
  line-height: 1.6;
  color: var(--mut);
  text-wrap: pretty;
}

.chm-16__note code {
  font: 11px ui-monospace,Menlo,Consolas,monospace;
  background: oklch(0.93 0.005 260);
  border-radius: 5px;
  padding: 1.5px 6px;
}

@media (prefers-reduced-motion: reduce) {
  .chm-16 * {
    transition-duration: .01s !important;
  }
}
```

## JavaScript
```js
(function () {
  /* Stand-in for bootstrap.bundle.min.js collapse — same class dance, same timing. */
  document.querySelectorAll('.chm-16__frame').forEach(function (frame) {
    if (frame.dataset.bound) return; frame.dataset.bound = '1';
    var btn = frame.querySelector('[data-bs-toggle="collapse"]'),
        panel = frame.querySelector(btn.getAttribute('data-bs-target'));
    btn.addEventListener('click', function () {
      var opening = !panel.classList.contains('show');
      btn.setAttribute('aria-expanded', String(opening));
      panel.classList.remove('collapse');
      panel.classList.add('collapsing');
      panel.style.height = opening ? '0' : panel.scrollHeight + 'px';
      requestAnimationFrame(function () {
        panel.style.height = opening ? panel.scrollHeight + 'px' : '0';
      });
      setTimeout(function () {
        panel.classList.remove('collapsing');
        panel.classList.add('collapse');
        panel.classList.toggle('show', opening);
        panel.style.height = '';
      }, 300);
    });
  });
})();
```

How this works

Bootstrap's navbar is a contract between five classes and two data attributes:

<nav class="navbar navbar-expand-lg bg-body-tertiary">
  <div class="container-fluid">
    <a class="navbar-brand" href="#">Brand</a>
    <button class="navbar-toggler" type="button"
            data-bs-toggle="collapse" data-bs-target="#mainNav"
            aria-controls="mainNav" aria-expanded="false"
            aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="mainNav">
      <ul class="navbar-nav ms-auto"> …nav-item / nav-link… </ul>
    </div>
  </div>
</nav>

navbar-expand-lg = burger below 992px, row above (swap lg for md/sm/xl to move it). data-bs-target must be a SELECTOR (#mainNav, hash included) while aria-controls is an ID (no hash) — mixing those up is fix #1. Fix #2: the collapse div needs BOTH collapse and navbar-collapse — the first is the plugin hook, the second the responsive styling. Fix #3: the toggler does nothing without bootstrap.bundle.min.js — the bundle build, which includes Popper; the plain build silently ships no collapse plugin. Bootstrap's JS updates aria-expanded for you — it's one of the few frameworks that gets that right by default.

The default toggler is a static SVG background-image. The docs' replacement (used live in this card) swaps it for three animated bars in ~10 lines, keyed off Bootstrap's own aria-expanded state — no plugin changes.

This card renders through a scoped shim replicating the relevant 5.3 styles plus a 15-line stand-in for the collapse plugin, so the demo behaves exactly as Bootstrap's would; in your project, load real Bootstrap and delete both.

Make it yours

  • Breakpoint: navbar-expand-{sm|md|lg|xl|xxl} — one class, no CSS. No expand class at all = always-burger (fullscreen-nav sites).
  • Alignment: ms-auto on navbar-nav pushes links right; mx-auto centers them; these are just margin utilities, nothing navbar-specific.
  • Smooth height animation comes free from the collapse plugin (it measures + transitions height); set .navbar-collapse{ transition-duration:.25s } to retune it.
  • Dark navbar in 5.3: data-bs-theme="dark" on the nav — the old navbar-dark class is deprecated; the attribute also recolors the default toggler icon.

Gotchas — read before shipping

  • 'Menu opens then instantly closes' = the button is inside a form or has no type="button", so it submits/reloads. Always declare the type.
  • Two navbars on one page must target different ids — data-bs-target="#mainNav" on both means one button drives both panels (collapse is selector-based, not proximity-based).
  • Overriding navbar styles requires loading YOUR css after bootstrap.min.css and matching specificity — .navbar-nav .nav-link, not .nav-link. Half of 'my styles don't apply' is load order.
  • The collapse animation needs the panel to start closed in markup (class="collapse", not "collapse show") — starting open then removing show on load causes the first-click double-fire confusion.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), color-mix(), @container, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

Bootstrap 5.3 itself supports Chrome/Edge/Firefox/Safari of the last ~3 years (no IE). The shim here mirrors it with container queries + oklch for in-card rendering only.

Search CodeFronts

Loading…