Open demo

CSS code

HTML
<div class="hb">
  <header class="hb-bar">
    <a href="#" class="hb-logo">Northwind</a>
    <button class="hb-burger" type="button" aria-label="Menu" aria-expanded="false" aria-controls="hb-drawer"><i></i><i></i><i></i></button>
  </header>
  <div class="hb-scrim" aria-hidden="true"></div>
  <nav class="hb-drawer" id="hb-drawer" aria-label="Main">
    <ul>
      <li><a href="#" aria-current="page">Home</a></li>
      <li><a href="#">Products</a></li>
      <li><a href="#">Pricing</a></li>
      <li><a href="#">Customers</a></li>
    </ul>
    <ul>
      <li><a href="#">Docs</a></li>
      <li><a href="#">Support</a></li>
    </ul>
    <a href="#" class="hb-cta">Start free trial</a>
  </nav>
</div>
CSS
/* Hamburger drawer: a disclosure button (aria-expanded + aria-controls) that slides in a navigation drawer under the header.
   The bars turn into a close mark; Esc, the scrim or the button closes it. */
.hb a{text-decoration:none}
.hb ul{list-style:none;margin:0;padding:0}
.hb-bar{display:flex;align-items:center;justify-content:space-between;padding:0 22px;height:62px;background:#fff;border-bottom:1px solid #e7e9f0;position:relative;z-index:30}
.hb-logo{font-weight:800;font-size:1.02rem;letter-spacing:-.02em;color:#101828}
.hb-burger{width:42px;height:42px;display:grid;place-items:center;align-content:center;gap:5px;padding:0;border:0;border-radius:10px;background:none;cursor:pointer}
.hb-burger:hover{background:#f2f4f8}
.hb-burger i{display:block;width:20px;height:2px;background:#101828;border-radius:2px;transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .18s}
.hb-burger[aria-expanded="true"] i:nth-child(1){transform:translateY(7px) rotate(45deg)}
.hb-burger[aria-expanded="true"] i:nth-child(2){opacity:0}
.hb-burger[aria-expanded="true"] i:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
.hb-drawer{position:fixed;top:62px;right:0;bottom:0;width:290px;max-width:82vw;background:#fff;border-left:1px solid #e7e9f0;padding:20px 16px;transform:translateX(101%);visibility:hidden;
  transition:transform .3s cubic-bezier(.4,0,.2,1),visibility .3s;z-index:20;display:flex;flex-direction:column;gap:3px;overflow-y:auto}
.hb[data-open] .hb-drawer{transform:none;visibility:visible}
.hb-drawer ul{display:flex;flex-direction:column;gap:3px}
.hb-drawer ul+ul::before{content:"";height:1px;background:#eef0f5;margin:10px 4px}
.hb-drawer a{display:block;padding:13px 14px;border-radius:10px;font-size:.95rem;font-weight:600;color:#344054}
.hb-drawer a:hover,.hb-drawer a[aria-current="page"]{background:#f2f4f8;color:#101828}
.hb-drawer .hb-cta{background:#4f46e5;color:#fff;text-align:center;margin-top:auto}
.hb-drawer .hb-cta:hover{background:#4338ca;color:#fff}
.hb-scrim{position:fixed;inset:62px 0 0;background:rgba(16,24,40,.36);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s;z-index:10}
.hb[data-open] .hb-scrim{opacity:1;visibility:visible}
.hb a:focus-visible,.hb button:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
@media (prefers-reduced-motion:reduce){.hb-burger i,.hb-drawer,.hb-scrim{transition:none}}
JavaScript
/* The button flips aria-expanded and a data-open attribute on .hb that CSS uses to show the drawer and scrim.
   Esc closes it and returns focus to the button; so does a click on the scrim. Tabbing out of the drawer closes it. */
document.querySelectorAll('.hb').forEach(root => {
  const btn = root.querySelector('.hb-burger'), drawer = document.getElementById(btn.getAttribute('aria-controls'));
  const set = open => { btn.setAttribute('aria-expanded', open); root.toggleAttribute('data-open', open); };
  btn.addEventListener('click', () => set(btn.getAttribute('aria-expanded') !== 'true'));
  root.querySelector('.hb-scrim').addEventListener('click', () => { set(false); btn.focus(); });
  root.addEventListener('keydown', e => { if (e.key === 'Escape' && root.hasAttribute('data-open')) { set(false); btn.focus(); } });
  drawer.addEventListener('focusout', e => { if (e.relatedTarget && !root.contains(e.relatedTarget)) set(false); });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="hb">
  <header class="hb-bar navbar">
    <a class="navbar-brand" href="#">Northwind</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#hb-drawer" aria-label="Menu" aria-expanded="false" aria-controls="hb-drawer"><i></i><i></i><i></i></button>
  </header>
  <div class="offcanvas offcanvas-end hb-drawer" tabindex="-1" id="hb-drawer" aria-label="Menu">
    <nav class="offcanvas-body" aria-label="Main">
      <ul class="nav flex-column">
        <li class="nav-item"><a class="nav-link active" href="#" aria-current="page">Home</a></li>
        <li class="nav-item"><a class="nav-link" href="#">Products</a></li>
        <li class="nav-item"><a class="nav-link" href="#">Pricing</a></li>
        <li class="nav-item"><a class="nav-link" href="#">Customers</a></li>
      </ul>
      <ul class="nav flex-column">
        <li class="nav-item"><a class="nav-link" href="#">Docs</a></li>
        <li class="nav-item"><a class="nav-link" href="#">Support</a></li>
      </ul>
      <a class="btn hb-cta" href="#">Start free trial</a>
    </nav>
  </div>
</div>
CSS
/* Bootstrap .navbar whose toggler opens an .offcanvas-end drawer below the bar. The Offcanvas plugin traps focus,
   closes on Esc or the backdrop and returns focus to the toggler; a few lines of script keep aria-expanded in sync
   so the bars can turn into a close mark. The bar sits above the backdrop, so the close mark stays clickable. */
.hb-bar{--bs-navbar-padding-x:22px;--bs-navbar-padding-y:0;--bs-navbar-brand-color:#101828;--bs-navbar-brand-hover-color:#101828;--bs-navbar-brand-font-size:1.02rem;--bs-navbar-brand-padding-y:0;--bs-navbar-brand-margin-end:0;
  height:62px;background:#fff;border-bottom:1px solid #e7e9f0;position:relative;z-index:1050}
.hb-bar .navbar-brand{font-weight:800;letter-spacing:-.02em;border-radius:4px}
.hb-bar .navbar-toggler{width:42px;height:42px;display:grid;place-items:center;align-content:center;gap:5px;padding:0;border:0;border-radius:10px}
.hb-bar .navbar-toggler:hover{background:#f2f4f8}
.hb-bar .navbar-toggler i{display:block;width:20px;height:2px;background:#101828;border-radius:2px;transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .18s}
.hb-bar .navbar-toggler[aria-expanded="true"] i:nth-child(1){transform:translateY(7px) rotate(45deg)}
.hb-bar .navbar-toggler[aria-expanded="true"] i:nth-child(2){opacity:0}
.hb-bar .navbar-toggler[aria-expanded="true"] i:nth-child(3){transform:translateY(-7px) rotate(-45deg)}
.hb-drawer.offcanvas{--bs-offcanvas-width:290px;--bs-offcanvas-border-color:#e7e9f0;--bs-offcanvas-transition:transform .3s cubic-bezier(.4,0,.2,1);top:62px;max-width:82vw}
.hb-drawer:focus{outline:0}
.hb-drawer .offcanvas-body{display:flex;flex-direction:column;gap:3px;padding:20px 16px}
.hb-drawer .nav{--bs-nav-link-padding-x:14px;--bs-nav-link-padding-y:13px;--bs-nav-link-font-size:.95rem;--bs-nav-link-font-weight:600;--bs-nav-link-color:#344054;--bs-nav-link-hover-color:#101828;gap:3px}
.hb-drawer .nav+.nav::before{content:"";height:1px;background:#eef0f5;margin:10px 4px}
.hb-drawer .nav-link{border-radius:10px;line-height:normal}
.hb-drawer .nav-link:hover,.hb-drawer .nav-link.active{background:#f2f4f8;color:#101828}
.hb-drawer .hb-cta{--bs-btn-padding-x:14px;--bs-btn-padding-y:13px;--bs-btn-font-size:.95rem;--bs-btn-font-weight:600;--bs-btn-line-height:normal;--bs-btn-border-radius:10px;--bs-btn-border-width:0;
  --bs-btn-color:#fff;--bs-btn-bg:#4f46e5;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#4338ca;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4338ca;margin-top:auto}
.offcanvas-backdrop{top:62px;background-color:#101828} /* starts under the 62px bar */
.offcanvas-backdrop.show{opacity:.36}
.hb a:focus-visible,.hb button:focus-visible{outline:2px solid #4f46e5;outline-offset:2px;box-shadow:none}
@media (prefers-reduced-motion:reduce){.hb-bar .navbar-toggler i,.hb-drawer.offcanvas{transition:none}}
JavaScript
/* Offcanvas does not manage aria-expanded on its toggler, so keep it in sync (the close mark is drawn from it). */
document.querySelectorAll('.hb .offcanvas').forEach(drawer => {
  const toggler = document.querySelector(`[data-bs-target="#${drawer.id}"]`);
  drawer.addEventListener('show.bs.offcanvas', () => toggler.setAttribute('aria-expanded', 'true'));
  drawer.addEventListener('hide.bs.offcanvas', () => toggler.setAttribute('aria-expanded', 'false'));
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="hb group/hb">
  <header class="relative z-30 flex h-[62px] items-center justify-between border-b border-hb-line bg-white px-[22px]">
    <a href="#" class="text-[1.02rem] font-extrabold tracking-[-.02em] text-hb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent">Northwind</a>
    <button class="hb-burger group/btn grid size-[42px] cursor-pointer place-items-center content-center gap-[5px] rounded-[10px] hover:bg-hb-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent" type="button" aria-label="Menu" aria-expanded="false" aria-controls="hb-drawer">
      <i class="block h-0.5 w-5 rounded-xs bg-hb-ink transition-transform duration-[280ms] ease-hb group-aria-expanded/btn:translate-y-[7px] group-aria-expanded/btn:rotate-45 motion-reduce:transition-none"></i>
      <i class="block h-0.5 w-5 rounded-xs bg-hb-ink transition-opacity duration-[180ms] group-aria-expanded/btn:opacity-0 motion-reduce:transition-none"></i>
      <i class="block h-0.5 w-5 rounded-xs bg-hb-ink transition-transform duration-[280ms] ease-hb group-aria-expanded/btn:-translate-y-[7px] group-aria-expanded/btn:-rotate-45 motion-reduce:transition-none"></i>
    </button>
  </header>
  <div class="hb-scrim invisible fixed inset-x-0 top-[62px] bottom-0 z-10 bg-[rgba(16,24,40,.36)] opacity-0 transition-[opacity,visibility] duration-300 group-data-[open]/hb:visible group-data-[open]/hb:opacity-100 motion-reduce:transition-none" aria-hidden="true"></div>
  <nav class="invisible fixed top-[62px] right-0 bottom-0 z-20 flex w-[290px] max-w-[82vw] translate-x-[101%] flex-col gap-[3px] overflow-y-auto border-l border-hb-line bg-white px-4 py-5 transition-[translate,visibility] duration-300 ease-hb group-data-[open]/hb:visible group-data-[open]/hb:translate-x-0 motion-reduce:transition-none" id="hb-drawer" aria-label="Main">
    <ul class="flex flex-col gap-[3px]">
      <li><a class="block rounded-[10px] bg-hb-soft px-3.5 py-[13px] text-[.95rem] font-semibold text-hb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent" href="#" aria-current="page">Home</a></li>
      <li><a class="block rounded-[10px] px-3.5 py-[13px] text-[.95rem] font-semibold text-hb-text hover:bg-hb-soft hover:text-hb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent" href="#">Products</a></li>
      <li><a class="block rounded-[10px] px-3.5 py-[13px] text-[.95rem] font-semibold text-hb-text hover:bg-hb-soft hover:text-hb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent" href="#">Pricing</a></li>
      <li><a class="block rounded-[10px] px-3.5 py-[13px] text-[.95rem] font-semibold text-hb-text hover:bg-hb-soft hover:text-hb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent" href="#">Customers</a></li>
    </ul>
    <ul class="flex flex-col gap-[3px] before:mx-1 before:my-2.5 before:h-px before:bg-[#eef0f5] before:content-['']">
      <li><a class="block rounded-[10px] px-3.5 py-[13px] text-[.95rem] font-semibold text-hb-text hover:bg-hb-soft hover:text-hb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent" href="#">Docs</a></li>
      <li><a class="block rounded-[10px] px-3.5 py-[13px] text-[.95rem] font-semibold text-hb-text hover:bg-hb-soft hover:text-hb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent" href="#">Support</a></li>
    </ul>
    <a href="#" class="mt-auto block rounded-[10px] bg-hb-accent px-3.5 py-[13px] text-center text-[.95rem] font-semibold text-white hover:bg-[#4338ca] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hb-accent">Start free trial</a>
  </nav>
</div>
CSS
@theme {
  --color-hb-ink: #101828;
  --color-hb-text: #344054;
  --color-hb-soft: #f2f4f8;
  --color-hb-line: #e7e9f0;
  --color-hb-accent: #4f46e5;
  --ease-hb: cubic-bezier(.4,0,.2,1);
}
JavaScript
/* The button flips aria-expanded and a data-open attribute on .hb that group-data-[open] utilities use to show the drawer
   and scrim. Esc closes it and returns focus to the button; so does a click on the scrim. Tabbing out of the drawer closes it. */
document.querySelectorAll('.hb').forEach(root => {
  const btn = root.querySelector('.hb-burger'), drawer = document.getElementById(btn.getAttribute('aria-controls'));
  const set = open => { btn.setAttribute('aria-expanded', open); root.toggleAttribute('data-open', open); };
  btn.addEventListener('click', () => set(btn.getAttribute('aria-expanded') !== 'true'));
  root.querySelector('.hb-scrim').addEventListener('click', () => { set(false); btn.focus(); });
  root.addEventListener('keydown', e => { if (e.key === 'Escape' && root.hasAttribute('data-open')) { set(false); btn.focus(); } });
  drawer.addEventListener('focusout', e => { if (e.relatedTarget && !root.contains(e.relatedTarget)) set(false); });
});

About this set

A mobile-style header with a logo and a three-bar hamburger button. Pressing it turns the bars into a close mark, slides a 290 pixel drawer in from the right under the header and dims the page with a scrim. The drawer lists Home, Products, Pricing and Customers, then Docs and Support after a thin rule, with a full-width indigo Start free trial button pinned to the bottom. Use it as the main navigation on small screens, or at every width for a minimal site. The button is a disclosure with aria-expanded and aria-controls, so screen readers hear Menu, collapsed or expanded, and the close mark is drawn from that same attribute. Esc or a tap on the scrim closes the drawer and returns focus to the button, tabbing out of the drawer closes it, and the hidden drawer is removed from the tab order. The drawer is capped at 82 percent of the viewport width so a strip of the page always shows. The Bootstrap version uses .navbar with an .offcanvas-end drawer, whose plugin also traps focus while it is open, and the Tailwind version uses group-data utilities.

What’s included

  • Hamburger bars that turn into a close mark
  • Drawer slides in under the header with a scrim
  • Esc, the scrim or the button closes it
  • Hidden drawer links stay out of the tab order
  • Drawer capped at 82% of the screen width

Accessibility

The Menu button reports its state with aria-expanded and controls the drawer, which is a nav labelled Main with the current page marked by aria-current. Esc or the scrim closes the drawer and focus returns to the button; in Bootstrap the offcanvas also keeps focus inside while open. Links and the button show a 2px indigo outline on keyboard focus, and the slide, fade and bar animations are removed under prefers-reduced-motion.

Customise it

Change the drawer width, colours and scrim in .hb-drawer and .hb-scrim (plain CSS), the --bs-offcanvas-* variables, .offcanvas-backdrop and --bs-nav-link-* values (Bootstrap) or the --color-hb-* theme colours and w-[290px] (Tailwind).