Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
  <symbol id="i-chev-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 15 6-6 6 6"/></symbol>
  <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>
  <symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7a1.5 1.5 0 0 1 1.5-1.5h4.3l2 2.5H19a1.5 1.5 0 0 1 1.5 1.5v8.5A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z"/></symbol>
  <symbol id="i-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="5" rx="1.2"/><path d="M5 9v9.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V9M10 13h4"/></symbol>
  <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M5.5 7l1 12a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12M9 7V4.5h6V7"/></symbol>
  <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h7l4 4v12a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z"/><path d="M13.5 3.5v4h4"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>
  <symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4.5 20.5c.8-4 3.7-6 7.5-6s6.7 2 7.5 6"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
  <symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 3.5 10 14M20.5 3.5l-6.5 17-4-6.5-6.5-4z"/></symbol>
  <symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.2 3.5h7.6l5.4 5.4v7.6l-5.4 5.4H8.2l-5.4-5.4V8.9z"/><path d="M12 8v5M12 16.2h.01"/></symbol>
  <symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 13.5h5l1.5 2.5h4l1.5-2.5h5"/><path d="M5.8 5.5h12.4l2.3 8v5.5a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1v-5.5z"/></symbol>
</svg>
<div><span class="lbl">Split button with a selectable default</span>
<div class="stage" style="--stage-h:340px">
<div class="sb-wrap"><div class="sb sb-green">
<button class="sb-btn" type="button"><svg aria-hidden="true"><use href="#i-check"/></svg><span data-choice="sb-a">Squash and merge</span></button><button class="sb-btn caret" id="sb-a-btn" type="button" aria-label="Choose merge method" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-a"><svg class="sb-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="sb-menu wide" role="menu" id="sb-a" aria-label="Merge method" hidden>
<button type="button" class="sb-item rich" role="menuitemradio" tabindex="-1" aria-checked="false" aria-labelledby="sb-a-t1" aria-describedby="sb-a-d1" data-choice-text="Merge pull request"><svg class="sb-chk" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sb-title" id="sb-a-t1">Create a merge commit</span><span class="sb-desc" id="sb-a-d1">All 6 commits from this branch are added to main with a merge commit.</span></span></button>
<button type="button" class="sb-item rich" role="menuitemradio" tabindex="-1" aria-checked="true" aria-labelledby="sb-a-t2" aria-describedby="sb-a-d2" data-choice-text="Squash and merge"><svg class="sb-chk" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sb-title" id="sb-a-t2">Squash and merge</span><span class="sb-desc" id="sb-a-d2">The 6 commits are combined into one commit on main.</span></span></button>
<button type="button" class="sb-item rich" role="menuitemradio" tabindex="-1" aria-checked="false" aria-labelledby="sb-a-t3" aria-describedby="sb-a-d3" data-choice-text="Rebase and merge"><svg class="sb-chk" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sb-title" id="sb-a-t3">Rebase and merge</span><span class="sb-desc" id="sb-a-d3">The 6 commits are rebased and added to main one by one.</span></span></button>
</div>
</div></div>
</div></div>
<div><span class="lbl">Button group with a dropdown</span>
<div class="stage" style="--stage-h:340px">
<div class="sb-wrap"><div class="sb" role="group" aria-label="Message actions"><button class="sb-btn icon" type="button" aria-label="Archive"><svg aria-hidden="true"><use href="#i-archive"/></svg></button><button class="sb-btn icon" type="button" aria-label="Report spam"><svg aria-hidden="true"><use href="#i-alert"/></svg></button><button class="sb-btn icon" type="button" aria-label="Delete"><svg aria-hidden="true"><use href="#i-trash"/></svg></button><div class="sb-nest">
<button class="sb-btn" id="sb-b-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-b">Move to<svg class="sb-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="sb-menu end" role="menu" id="sb-b" aria-labelledby="sb-b-btn" hidden>
<div role="group" aria-labelledby="sb-b-h">
<div class="sb-head" id="sb-b-h">Move to</div>
<button type="button" class="sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-inbox"/></svg>Inbox</button>
<button type="button" class="sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Clients</button>
<button type="button" class="sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Invoices</button>
<button type="button" class="sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Receipts 2026</button>
</div>
<div class="sb-sep" role="separator"></div>
<button type="button" class="sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-plus"/></svg>New folder…</button>
</div>
</div></div></div>
</div></div>
<div class="full"><span class="lbl">Drop-up split button in a composer</span>
<div class="stage bottom" style="--stage-h:340px">
<div class="sb-wrap sb-compose"><p>Hi Priya, the revised quote for the Lisbon offsite is attached. Happy to walk through it on Thursday.</p><span class="sb-file"><svg aria-hidden="true"><use href="#i-file"/></svg><b>Quote-Lisbon-v3.pdf</b>184 KB</span><div class="sb-foot"><small>Draft saved at 10:42</small><div class="sb sb-blue">
<button class="sb-btn" type="button"><svg aria-hidden="true"><use href="#i-send"/></svg>Send</button><button class="sb-btn caret" id="sb-c-btn" type="button" aria-label="More send options" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-c"><svg class="sb-chev" aria-hidden="true"><use href="#i-chev-up"/></svg></button>
<div class="sb-menu up end" role="menu" id="sb-c" aria-label="Send options" hidden>
<button type="button" class="sb-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Shift+Meta+Enter"><svg aria-hidden="true"><use href="#i-clock"/></svg><span>Schedule send…</span><kbd class="sb-kbd" aria-hidden="true">⇧⌘↵</kbd></button>
<button type="button" class="sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-archive"/></svg>Send and archive</button>
<button type="button" class="sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-user"/></svg>Send a test to me</button>
</div>
</div></div></div>
</div></div>
CSS
/* Split buttons and button groups: the main action is its own button; a second button with aria-haspopup opens the menu. */
.sb-wrap{--sb-ink:#1b2230;--sb-muted:#596275;--sb-line:#e3e6ec;--sb-hover:#f1f4f9;--sb-accent:#2563eb;--sb-accent-dark:#1d4ed8;--sb-green:#1a7f37;--sb-green-dark:#136a2c;font-size:.875rem}
.sb{position:relative;display:inline-flex}
.sb-btn{display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 14px;border:1px solid #d0d5de;background:#fff;color:var(--sb-ink);font:inherit;font-weight:600;
  cursor:pointer;transition:background-color .15s}
.sb-btn:hover{background:#f6f8fb}
.sb-btn svg{width:16px;height:16px;flex:none;transition:rotate .2s}
.sb>.sb-btn:first-child{border-radius:10px 0 0 10px}
.sb>.sb-btn+.sb-btn,.sb>.sb-nest{margin-left:-1px}
.sb>.sb-btn.caret,.sb-nest>.sb-btn{border-radius:0 10px 10px 0}
.sb-nest{position:relative;display:inline-flex}
.sb-btn.caret,.sb-btn.icon{width:40px;padding:0;justify-content:center}
.sb-btn.icon{color:var(--sb-muted)}
.sb-btn.icon:hover{color:var(--sb-ink)}
.sb-btn[aria-expanded="true"]{background:var(--sb-hover)}
.sb-btn[aria-expanded="true"] .sb-chev{rotate:180deg}
.sb-green .sb-btn{border-color:var(--sb-green-dark);background:var(--sb-green);color:#fff}
.sb-blue .sb-btn{border-color:var(--sb-accent-dark);background:var(--sb-accent);color:#fff}
.sb-green .sb-btn:hover,.sb-green .sb-btn[aria-expanded="true"]{background:var(--sb-green-dark)}
.sb-blue .sb-btn:hover,.sb-blue .sb-btn[aria-expanded="true"]{background:var(--sb-accent-dark)}
.sb-green .caret,.sb-blue .caret{border-left-color:rgba(255,255,255,.4)}
.sb-btn:focus-visible{position:relative;z-index:1;outline:2px solid var(--sb-accent);outline-offset:2px}
.sb-green .sb-btn:focus-visible{outline-color:var(--sb-green)}

.sb-menu{position:absolute;z-index:20;top:calc(100% + 6px);left:0;min-width:220px;padding:6px;background:#fff;border:1px solid var(--sb-line);border-radius:12px;
  box-shadow:0 1px 2px rgba(16,24,40,.04),0 14px 34px -10px rgba(16,24,40,.24);animation:sb-in .14s ease-out}
.sb-menu[hidden]{display:none}
.sb-menu.up{top:auto;bottom:calc(100% + 6px);animation-name:sb-up}
@keyframes sb-in{from{opacity:0;translate:0 -4px}}
@keyframes sb-up{from{opacity:0;translate:0 4px}}
.sb-item{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border:0;border-radius:8px;background:none;color:var(--sb-ink);font:inherit;font-weight:500;
  line-height:1.4;text-align:left;text-decoration:none;white-space:nowrap;cursor:pointer}
.sb-item>svg{width:16px;height:16px;flex:none;color:var(--sb-muted)}
.sb-item:hover,.sb-item:focus{background:var(--sb-hover);outline:none}
.sb-item:focus-visible{outline:2px solid var(--sb-accent);outline-offset:-2px}
.sb-kbd{margin-left:auto;padding-left:18px;color:var(--sb-muted);font:500 .75rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.sb-sep{height:1px;margin:6px -6px;background:var(--sb-line)}
.sb-head{padding:6px 10px 4px;color:var(--sb-muted);font-size:.6875rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase}

.sb-menu.end{left:auto;right:0}
.sb-menu.wide{width:344px;max-width:calc(100vw - 72px)}
.sb-item.rich{align-items:flex-start;gap:10px;padding:10px;white-space:normal}
.sb-chk{width:16px;height:16px;flex:none;margin-top:2px;color:var(--sb-green);visibility:hidden}
.sb-item[aria-checked="true"] .sb-chk{visibility:visible}
.sb-title{display:block;font-weight:600}
.sb-desc{display:block;margin-top:2px;color:var(--sb-muted);font-size:.8rem;font-weight:400;line-height:1.45}
.sb-compose{display:flex;flex-direction:column;width:100%;background:#fff;border:1px solid var(--sb-line);border-radius:14px;box-shadow:0 1px 2px rgba(16,24,40,.05)}
.sb-compose p{margin:0;padding:16px 18px 8px;color:var(--sb-ink);line-height:1.55}
.sb-file{display:inline-flex;align-items:center;gap:8px;margin:0 18px 14px;align-self:flex-start;padding:6px 10px;border:1px solid var(--sb-line);border-radius:9px;background:#f8f9fb;font-size:.8rem;color:var(--sb-muted)}
.sb-file svg{width:16px;height:16px;color:#c2410c}
.sb-file b{color:var(--sb-ink);font-weight:600}
.sb-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px 10px 18px;border-top:1px solid var(--sb-line)}
.sb-foot small{color:var(--sb-muted);font-size:.8rem}
.stage.bottom{display:flex;align-items:flex-end}

@media (prefers-reduced-motion:reduce){.sb-menu{animation:none}.sb-btn,.sb-btn svg{transition:none}}
JavaScript
// Menu button (WAI-ARIA APG pattern). A trigger has aria-haspopup="menu", aria-expanded and aria-controls="<menu id>".
// Click, Enter, Space or ArrowDown opens the menu (ArrowUp opens it on the last item). Inside: ArrowUp/ArrowDown move,
// Home/End jump, typing a letter jumps to the next item that starts with it, Esc closes and returns focus to the trigger,
// Tab or a click outside closes. Choosing an item closes the menu; checkbox items stay open so you can tick several.
(() => {
  const OPEN = '[aria-haspopup="menu"][aria-expanded="true"]';
  const menuOf = t => document.getElementById(t.getAttribute('aria-controls'));
  const ownerOf = m => m && document.querySelector(`[aria-controls="${m.id}"][aria-expanded="true"]`);
  const itemsOf = m => [...m.querySelectorAll('[role^="menuitem"]')]
    .filter(i => i.closest('[role="menu"]') === m && i.getAttribute('aria-disabled') !== 'true');

  function close(t, focus) {
    const m = menuOf(t);
    m.querySelectorAll(OPEN).forEach(s => close(s));
    t.setAttribute('aria-expanded', 'false');
    m.hidden = true;
    if (focus) t.focus();
  }
  function open(t, at) {
    const level = t.closest('[role="menu"]');
    document.querySelectorAll(OPEN).forEach(o => { if (o !== t && o.closest('[role="menu"]') === level) close(o); });
    t.setAttribute('aria-expanded', 'true');
    menuOf(t).hidden = false;
    if (at !== undefined) itemsOf(menuOf(t)).at(at)?.focus();
  }
  const closeAll = target => document.querySelectorAll(OPEN).forEach(t => {
    if (!t.closest('[role="menu"]') && !t.contains(target) && !menuOf(t).contains(target)) close(t);
  });

  document.addEventListener('click', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t) {
      if (!t.closest('[role="menu"]') && t.getAttribute('aria-expanded') === 'true') close(t);
      else open(t, e.detail === 0 ? 0 : undefined); // keyboard clicks (Enter/Space) move focus into the menu
      return;
    }
    const item = e.target.closest('[role^="menuitem"]');
    if (item && item.getAttribute('aria-disabled') !== 'true') {
      const role = item.getAttribute('role');
      if (role === 'menuitemcheckbox') { item.setAttribute('aria-checked', item.getAttribute('aria-checked') !== 'true'); return; }
      if (role === 'menuitemradio') {
        const set = item.closest('[role="group"]') || item.closest('[role="menu"]');
        set.querySelectorAll('[role="menuitemradio"]').forEach(r => r.setAttribute('aria-checked', r === item));
        // Copy the choice onto the element that shows it, e.g. the split button's label.
        document.querySelectorAll(`[data-choice="${set.closest('[role="menu"]').id}"]`).forEach(el => {
          el.textContent = item.dataset.choiceText;
          if (item.dataset.value) el.dataset.value = item.dataset.value;
        });
      }
      let t = ownerOf(item.closest('[role="menu"]'));
      while (t && t.closest('[role="menu"]')) t = ownerOf(t.closest('[role="menu"]'));
      if (t) close(t, true);
      return;
    }
    closeAll(e.target);
  });

  document.addEventListener('keydown', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t && !t.closest('[role="menu"]')) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); open(t, e.key === 'ArrowDown' ? 0 : -1); }
      else if (e.key === 'Escape' && t.getAttribute('aria-expanded') === 'true') close(t);
      return;
    }
    const m = e.target.closest('[role="menu"]');
    if (!m) return;
    const items = itemsOf(m), i = items.indexOf(e.target), owner = ownerOf(m);
    let next;
    switch (e.key) {
      case 'ArrowDown': next = items[(i + 1) % items.length]; break;
      case 'ArrowUp': next = items.at(i <= 0 ? -1 : i - 1); break;
      case 'Home': next = items[0]; break;
      case 'End': next = items.at(-1); break;
      case 'Escape': if (owner) { e.preventDefault(); close(owner, true); } return;
      case 'Tab': closeAll(null); return;
      case ' ': if (e.target.tagName === 'A') { e.preventDefault(); e.target.click(); } return;
      default:
        if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;
        next = [...items.slice(i + 1), ...items.slice(0, i + 1)]
          .find(x => x.textContent.trim().toLowerCase().startsWith(e.key.toLowerCase()));
    }
    e.preventDefault();
    next?.focus();
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
  <symbol id="i-chev-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 15 6-6 6 6"/></symbol>
  <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>
  <symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7a1.5 1.5 0 0 1 1.5-1.5h4.3l2 2.5H19a1.5 1.5 0 0 1 1.5 1.5v8.5A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z"/></symbol>
  <symbol id="i-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="5" rx="1.2"/><path d="M5 9v9.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V9M10 13h4"/></symbol>
  <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M5.5 7l1 12a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12M9 7V4.5h6V7"/></symbol>
  <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h7l4 4v12a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z"/><path d="M13.5 3.5v4h4"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>
  <symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4.5 20.5c.8-4 3.7-6 7.5-6s6.7 2 7.5 6"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
  <symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 3.5 10 14M20.5 3.5l-6.5 17-4-6.5-6.5-4z"/></symbol>
  <symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.2 3.5h7.6l5.4 5.4v7.6l-5.4 5.4H8.2l-5.4-5.4V8.9z"/><path d="M12 8v5M12 16.2h.01"/></symbol>
  <symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 13.5h5l1.5 2.5h4l1.5-2.5h5"/><path d="M5.8 5.5h12.4l2.3 8v5.5a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1v-5.5z"/></symbol>
</svg>
<div><span class="lbl">Split button with a selectable default</span>
<div class="stage" style="--stage-h:340px">
<div class="sb-wrap"><div class="btn-group sb sb-green">
<button class="btn" type="button"><svg aria-hidden="true"><use href="#i-check"/></svg><span data-choice="sb-a">Squash and merge</span></button><button class="btn caret dropdown-toggle-split" id="sb-a-btn" type="button" data-bs-toggle="dropdown" data-bs-offset="0,6" data-bs-reference="parent" aria-label="Choose merge method" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-a"><svg class="sb-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="dropdown-menu sb-menu wide" role="menu" id="sb-a" aria-label="Merge method">
<button type="button" class="dropdown-item sb-item rich" role="menuitemradio" tabindex="-1" aria-checked="false" aria-labelledby="sb-a-t1" aria-describedby="sb-a-d1" data-choice-text="Merge pull request"><svg class="sb-chk" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sb-title" id="sb-a-t1">Create a merge commit</span><span class="sb-desc" id="sb-a-d1">All 6 commits from this branch are added to main with a merge commit.</span></span></button>
<button type="button" class="dropdown-item sb-item rich" role="menuitemradio" tabindex="-1" aria-checked="true" aria-labelledby="sb-a-t2" aria-describedby="sb-a-d2" data-choice-text="Squash and merge"><svg class="sb-chk" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sb-title" id="sb-a-t2">Squash and merge</span><span class="sb-desc" id="sb-a-d2">The 6 commits are combined into one commit on main.</span></span></button>
<button type="button" class="dropdown-item sb-item rich" role="menuitemradio" tabindex="-1" aria-checked="false" aria-labelledby="sb-a-t3" aria-describedby="sb-a-d3" data-choice-text="Rebase and merge"><svg class="sb-chk" aria-hidden="true"><use href="#i-check"/></svg><span><span class="sb-title" id="sb-a-t3">Rebase and merge</span><span class="sb-desc" id="sb-a-d3">The 6 commits are rebased and added to main one by one.</span></span></button>
</div>
</div></div>
</div></div>
<div><span class="lbl">Button group with a dropdown</span>
<div class="stage" style="--stage-h:340px">
<div class="sb-wrap"><div class="btn-group sb" role="group" aria-label="Message actions"><button class="btn icon" type="button" aria-label="Archive"><svg aria-hidden="true"><use href="#i-archive"/></svg></button><button class="btn icon" type="button" aria-label="Report spam"><svg aria-hidden="true"><use href="#i-alert"/></svg></button><button class="btn icon" type="button" aria-label="Delete"><svg aria-hidden="true"><use href="#i-trash"/></svg></button><div class="btn-group" role="group">
<button class="btn" id="sb-b-btn" type="button" data-bs-toggle="dropdown" data-bs-offset="0,6" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-b">Move to<svg class="sb-chev" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="dropdown-menu sb-menu dropdown-menu-end" role="menu" id="sb-b" aria-labelledby="sb-b-btn">
<div role="group" aria-labelledby="sb-b-h">
<div class="dropdown-header" id="sb-b-h">Move to</div>
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-inbox"/></svg>Inbox</button>
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Clients</button>
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Invoices</button>
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-folder"/></svg>Receipts 2026</button>
</div>
<hr class="dropdown-divider">
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-plus"/></svg>New folder…</button>
</div>
</div></div></div>
</div></div>
<div class="full"><span class="lbl">Drop-up split button in a composer</span>
<div class="stage bottom" style="--stage-h:340px">
<div class="sb-wrap sb-compose"><p>Hi Priya, the revised quote for the Lisbon offsite is attached. Happy to walk through it on Thursday.</p><span class="sb-file"><svg aria-hidden="true"><use href="#i-file"/></svg><b>Quote-Lisbon-v3.pdf</b>184 KB</span><div class="sb-foot"><small>Draft saved at 10:42</small><div class="btn-group dropup sb sb-blue">
<button class="btn" type="button"><svg aria-hidden="true"><use href="#i-send"/></svg>Send</button><button class="btn caret dropdown-toggle-split" id="sb-c-btn" type="button" data-bs-toggle="dropdown" data-bs-offset="0,6" aria-label="More send options" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-c"><svg class="sb-chev" aria-hidden="true"><use href="#i-chev-up"/></svg></button>
<div class="dropdown-menu sb-menu dropdown-menu-end" role="menu" id="sb-c" aria-label="Send options">
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1" aria-keyshortcuts="Shift+Meta+Enter"><svg aria-hidden="true"><use href="#i-clock"/></svg><span>Schedule send…</span><kbd class="sb-kbd" aria-hidden="true">⇧⌘↵</kbd></button>
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-archive"/></svg>Send and archive</button>
<button type="button" class="dropdown-item sb-item" role="menuitem" tabindex="-1"><svg aria-hidden="true"><use href="#i-user"/></svg>Send a test to me</button>
</div>
</div></div></div>
</div></div>
CSS
/* Bootstrap .btn-group + .dropdown-toggle-split, restyled through --bs-btn-* and --bs-dropdown-* variables. */
.sb-wrap{--sb-ink:#1b2230;--sb-muted:#596275;--sb-line:#e3e6ec;--sb-hover:#f1f4f9;--sb-accent:#2563eb;--sb-accent-dark:#1d4ed8;--sb-green:#1a7f37;--sb-green-dark:#136a2c;font-size:.875rem}
.sb .btn{--bs-btn-padding-x:14px;--bs-btn-padding-y:0;--bs-btn-font-size:.875rem;--bs-btn-font-weight:600;--bs-btn-color:var(--sb-ink);--bs-btn-bg:#fff;--bs-btn-border-color:#d0d5de;
  --bs-btn-border-radius:10px;--bs-btn-hover-color:var(--sb-ink);--bs-btn-hover-bg:#f6f8fb;--bs-btn-hover-border-color:#d0d5de;--bs-btn-active-color:var(--sb-ink);
  --bs-btn-active-bg:var(--sb-hover);--bs-btn-active-border-color:#d0d5de;display:inline-flex;align-items:center;gap:8px;height:40px}
.sb .btn svg{width:16px;height:16px;flex:none;transition:rotate .2s}
.sb .btn.caret,.sb .btn.icon{width:40px;padding:0;justify-content:center}
.sb .btn.icon{--bs-btn-color:var(--sb-muted)}
.sb .btn.show .sb-chev{rotate:180deg}
.sb-green .btn{--bs-btn-color:#fff;--bs-btn-bg:var(--sb-green);--bs-btn-border-color:var(--sb-green-dark);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--sb-green-dark);
  --bs-btn-hover-border-color:var(--sb-green-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--sb-green-dark);--bs-btn-active-border-color:var(--sb-green-dark)}
.sb-blue .btn{--bs-btn-color:#fff;--bs-btn-bg:var(--sb-accent);--bs-btn-border-color:var(--sb-accent-dark);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--sb-accent-dark);
  --bs-btn-hover-border-color:var(--sb-accent-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--sb-accent-dark);--bs-btn-active-border-color:var(--sb-accent-dark)}
.sb-green .caret,.sb-blue .caret{border-left-color:rgba(255,255,255,.4)}
.sb .btn:focus-visible{z-index:2;outline:2px solid var(--sb-accent);outline-offset:2px;box-shadow:none}
.sb-green .btn:focus-visible{outline-color:var(--sb-green)}

.sb-menu{--bs-dropdown-min-width:220px;--bs-dropdown-padding-x:6px;--bs-dropdown-padding-y:6px;--bs-dropdown-font-size:.875rem;--bs-dropdown-color:var(--sb-ink);
  --bs-dropdown-border-color:var(--sb-line);--bs-dropdown-border-radius:12px;--bs-dropdown-divider-bg:var(--sb-line);--bs-dropdown-divider-margin-y:6px;
  --bs-dropdown-link-color:var(--sb-ink);--bs-dropdown-link-hover-color:var(--sb-ink);--bs-dropdown-link-hover-bg:var(--sb-hover);--bs-dropdown-link-active-color:var(--sb-ink);
  --bs-dropdown-link-active-bg:var(--sb-hover);--bs-dropdown-item-padding-x:10px;--bs-dropdown-item-padding-y:8px;--bs-dropdown-item-border-radius:8px;
  --bs-dropdown-header-color:var(--sb-muted);--bs-dropdown-header-padding-x:10px;--bs-dropdown-header-padding-y:4px;
  box-shadow:0 1px 2px rgba(16,24,40,.04),0 14px 34px -10px rgba(16,24,40,.24)}
.sb-menu.show{animation:sb-in .14s ease-out}
.dropup .sb-menu.show{animation-name:sb-up}
@keyframes sb-in{from{opacity:0;translate:0 -4px}}
@keyframes sb-up{from{opacity:0;translate:0 4px}}
.sb-menu .dropdown-divider{margin-inline:-6px;opacity:1}
.sb-menu .dropdown-header{padding-top:6px;font-size:.6875rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
.sb-item{display:flex;align-items:center;gap:10px;font-weight:500;line-height:1.4}
.sb-item>svg{width:16px;height:16px;flex:none;color:var(--sb-muted)}
.sb-item:focus{outline:none}
.sb-item:focus-visible{outline:2px solid var(--sb-accent);outline-offset:-2px}
.sb-kbd{margin-left:auto;padding:0 0 0 18px;background:none;color:var(--sb-muted);font:500 .75rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}

.sb-menu.end{left:auto;right:0}
.sb-menu.wide{width:344px;max-width:calc(100vw - 72px)}
.sb-item.rich{align-items:flex-start;gap:10px;padding:10px;white-space:normal}
.sb-chk{width:16px;height:16px;flex:none;margin-top:2px;color:var(--sb-green);visibility:hidden}
.sb-item[aria-checked="true"] .sb-chk{visibility:visible}
.sb-title{display:block;font-weight:600}
.sb-desc{display:block;margin-top:2px;color:var(--sb-muted);font-size:.8rem;font-weight:400;line-height:1.45}
.sb-compose{display:flex;flex-direction:column;width:100%;background:#fff;border:1px solid var(--sb-line);border-radius:14px;box-shadow:0 1px 2px rgba(16,24,40,.05)}
.sb-compose p{margin:0;padding:16px 18px 8px;color:var(--sb-ink);line-height:1.55}
.sb-file{display:inline-flex;align-items:center;gap:8px;margin:0 18px 14px;align-self:flex-start;padding:6px 10px;border:1px solid var(--sb-line);border-radius:9px;background:#f8f9fb;font-size:.8rem;color:var(--sb-muted)}
.sb-file svg{width:16px;height:16px;color:#c2410c}
.sb-file b{color:var(--sb-ink);font-weight:600}
.sb-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 12px 10px 18px;border-top:1px solid var(--sb-line)}
.sb-foot small{color:var(--sb-muted);font-size:.8rem}
.stage.bottom{display:flex;align-items:flex-end}

@media (prefers-reduced-motion:reduce){.sb-menu.show{animation:none}.sb .btn svg{transition:none}}
JavaScript
// Radio items in a Bootstrap dropdown: Bootstrap closes the menu on click; this keeps aria-checked in step
// and copies the choice onto the element that shows it (data-choice="<menu id>").
document.addEventListener('click', e => {
  const item = e.target.closest('.dropdown-menu [role="menuitemradio"]');
  if (!item) return;
  const menu = item.closest('.dropdown-menu');
  menu.querySelectorAll('[role="menuitemradio"]').forEach(r => r.setAttribute('aria-checked', r === item));
  document.querySelectorAll(`[data-choice="${menu.id}"]`).forEach(el => {
    el.textContent = item.dataset.choiceText;
    if (item.dataset.value) el.dataset.value = item.dataset.value;
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="i-chev-down" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></symbol>
  <symbol id="i-chev-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m6 15 6-6 6 6"/></symbol>
  <symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></symbol>
  <symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 7a1.5 1.5 0 0 1 1.5-1.5h4.3l2 2.5H19a1.5 1.5 0 0 1 1.5 1.5v8.5A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z"/></symbol>
  <symbol id="i-archive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="5" rx="1.2"/><path d="M5 9v9.5A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5V9M10 13h4"/></symbol>
  <symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M5.5 7l1 12a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5l1-12M9 7V4.5h6V7"/></symbol>
  <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h7l4 4v12a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1z"/><path d="M13.5 3.5v4h4"/></symbol>
  <symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></symbol>
  <symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4.5 20.5c.8-4 3.7-6 7.5-6s6.7 2 7.5 6"/></symbol>
  <symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></symbol>
  <symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20.5 3.5 10 14M20.5 3.5l-6.5 17-4-6.5-6.5-4z"/></symbol>
  <symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.2 3.5h7.6l5.4 5.4v7.6l-5.4 5.4H8.2l-5.4-5.4V8.9z"/><path d="M12 8v5M12 16.2h.01"/></symbol>
  <symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 13.5h5l1.5 2.5h4l1.5-2.5h5"/><path d="M5.8 5.5h12.4l2.3 8v5.5a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1v-5.5z"/></symbol>
</svg>
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Split button with a selectable default</span>
<div class="relative min-h-[340px] rounded-[18px] border border-[#dfe3eb] bg-[#f8f9fc] bg-[radial-gradient(#d5dae4_1px,transparent_1px)] bg-size-[18px_18px] p-7 max-[600px]:p-5">
<div class="relative inline-flex text-[.875rem]">
<button class="inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#136a2c] bg-[#1a7f37] text-white hover:bg-[#136a2c] aria-expanded:bg-[#136a2c] focus-visible:outline-[#1a7f37] rounded-l-[10px] px-3.5" type="button"><svg class="size-4 flex-none" aria-hidden="true"><use href="#i-check"/></svg><span data-choice="sb-a">Squash and merge</span></button><button class="group inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#136a2c] bg-[#1a7f37] text-white hover:bg-[#136a2c] aria-expanded:bg-[#136a2c] focus-visible:outline-[#1a7f37] -ml-px w-10 justify-center rounded-r-[10px] border-l-white/40 p-0" id="sb-a-btn" type="button" aria-label="Choose merge method" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-a"><svg class="size-4 flex-none transition-[rotate] duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="absolute top-[calc(100%+6px)] left-0 z-20 min-w-[220px] rounded-xl border border-[#e3e6ec] bg-white p-1.5 shadow-[0_1px_2px_rgba(16,24,40,.04),0_14px_34px_-10px_rgba(16,24,40,.24)] animate-menu-in motion-reduce:animate-none w-[344px] max-w-[calc(100vw-72px)]" role="menu" id="sb-a" aria-label="Merge method" hidden>
<button type="button" class="flex w-full cursor-pointer group items-start gap-2.5 rounded-lg border-0 bg-transparent p-2.5 text-left leading-[1.4] font-medium whitespace-normal text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitemradio" tabindex="-1" aria-checked="false" aria-labelledby="sb-a-t1" aria-describedby="sb-a-d1" data-choice-text="Merge pull request"><svg class="invisible mt-0.5 size-4 flex-none text-[#1a7f37] group-aria-checked:visible" aria-hidden="true"><use href="#i-check"/></svg><span><span class="block font-semibold" id="sb-a-t1">Create a merge commit</span><span class="mt-0.5 block text-[.8rem] leading-[1.45] font-normal text-[#596275]" id="sb-a-d1">All 6 commits from this branch are added to main with a merge commit.</span></span></button>
<button type="button" class="flex w-full cursor-pointer group items-start gap-2.5 rounded-lg border-0 bg-transparent p-2.5 text-left leading-[1.4] font-medium whitespace-normal text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitemradio" tabindex="-1" aria-checked="true" aria-labelledby="sb-a-t2" aria-describedby="sb-a-d2" data-choice-text="Squash and merge"><svg class="invisible mt-0.5 size-4 flex-none text-[#1a7f37] group-aria-checked:visible" aria-hidden="true"><use href="#i-check"/></svg><span><span class="block font-semibold" id="sb-a-t2">Squash and merge</span><span class="mt-0.5 block text-[.8rem] leading-[1.45] font-normal text-[#596275]" id="sb-a-d2">The 6 commits are combined into one commit on main.</span></span></button>
<button type="button" class="flex w-full cursor-pointer group items-start gap-2.5 rounded-lg border-0 bg-transparent p-2.5 text-left leading-[1.4] font-medium whitespace-normal text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitemradio" tabindex="-1" aria-checked="false" aria-labelledby="sb-a-t3" aria-describedby="sb-a-d3" data-choice-text="Rebase and merge"><svg class="invisible mt-0.5 size-4 flex-none text-[#1a7f37] group-aria-checked:visible" aria-hidden="true"><use href="#i-check"/></svg><span><span class="block font-semibold" id="sb-a-t3">Rebase and merge</span><span class="mt-0.5 block text-[.8rem] leading-[1.45] font-normal text-[#596275]" id="sb-a-d3">The 6 commits are rebased and added to main one by one.</span></span></button>
</div>
</div>
</div></div>
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Button group with a dropdown</span>
<div class="relative min-h-[340px] rounded-[18px] border border-[#dfe3eb] bg-[#f8f9fc] bg-[radial-gradient(#d5dae4_1px,transparent_1px)] bg-size-[18px_18px] p-7 max-[600px]:p-5">
<div class="inline-flex text-[.875rem]" role="group" aria-label="Message actions"><button class="inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#d0d5de] bg-white text-[#1b2230] hover:bg-[#f6f8fb] aria-expanded:bg-[#f1f4f9] focus-visible:outline-[#2563eb] w-10 justify-center p-0 text-[#596275] hover:text-[#1b2230] rounded-l-[10px]" type="button" aria-label="Archive"><svg class="size-4" aria-hidden="true"><use href="#i-archive"/></svg></button><button class="inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#d0d5de] bg-white text-[#1b2230] hover:bg-[#f6f8fb] aria-expanded:bg-[#f1f4f9] focus-visible:outline-[#2563eb] w-10 justify-center p-0 text-[#596275] hover:text-[#1b2230] -ml-px" type="button" aria-label="Report spam"><svg class="size-4" aria-hidden="true"><use href="#i-alert"/></svg></button><button class="inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#d0d5de] bg-white text-[#1b2230] hover:bg-[#f6f8fb] aria-expanded:bg-[#f1f4f9] focus-visible:outline-[#2563eb] w-10 justify-center p-0 text-[#596275] hover:text-[#1b2230] -ml-px" type="button" aria-label="Delete"><svg class="size-4" aria-hidden="true"><use href="#i-trash"/></svg></button><div class="relative -ml-px inline-flex">
<button class="group inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#d0d5de] bg-white text-[#1b2230] hover:bg-[#f6f8fb] aria-expanded:bg-[#f1f4f9] focus-visible:outline-[#2563eb] rounded-r-[10px] px-3.5" id="sb-b-btn" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-b">Move to<svg class="size-4 flex-none transition-[rotate] duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none text-[#596275]" aria-hidden="true"><use href="#i-chev-down"/></svg></button>
<div class="absolute top-[calc(100%+6px)] left-0 z-20 min-w-[220px] rounded-xl border border-[#e3e6ec] bg-white p-1.5 shadow-[0_1px_2px_rgba(16,24,40,.04),0_14px_34px_-10px_rgba(16,24,40,.24)] animate-menu-in motion-reduce:animate-none right-0 left-auto" role="menu" id="sb-b" aria-labelledby="sb-b-btn" hidden>
<div role="group" aria-labelledby="sb-b-h">
<div class="px-2.5 pt-1.5 pb-1 text-[.6875rem] font-bold tracking-[.07em] text-[#596275] uppercase" id="sb-b-h">Move to</div>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-inbox"/></svg>Inbox</button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-folder"/></svg>Clients</button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-folder"/></svg>Invoices</button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-folder"/></svg>Receipts 2026</button>
</div>
<div class="-mx-1.5 my-1.5 h-px bg-[#e3e6ec]" role="separator"></div>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-plus"/></svg>New folder…</button>
</div>
</div></div>
</div></div>
<div class="col-span-full"><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Drop-up split button in a composer</span>
<div class="relative min-h-[340px] rounded-[18px] border border-[#dfe3eb] bg-[#f8f9fc] bg-[radial-gradient(#d5dae4_1px,transparent_1px)] bg-size-[18px_18px] p-7 max-[600px]:p-5 flex items-end">
<div class="flex w-full flex-col rounded-[14px] border border-[#e3e6ec] bg-white text-[.875rem] shadow-[0_1px_2px_rgba(16,24,40,.05)]"><p class="m-0 px-[18px] pt-4 pb-2 leading-[1.55] text-[#1b2230]">Hi Priya, the revised quote for the Lisbon offsite is attached. Happy to walk through it on Thursday.</p><span class="mx-[18px] mb-3.5 inline-flex items-center gap-2 self-start rounded-[9px] border border-[#e3e6ec] bg-[#f8f9fb] px-2.5 py-1.5 text-[.8rem] text-[#596275]"><svg class="size-4 text-[#c2410c]" aria-hidden="true"><use href="#i-file"/></svg><b class="font-semibold text-[#1b2230]">Quote-Lisbon-v3.pdf</b>184 KB</span><div class="flex items-center justify-between gap-3 border-t border-[#e3e6ec] py-2.5 pr-3 pl-[18px]"><small class="text-[.8rem] text-[#596275]">Draft saved at 10:42</small><div class="relative inline-flex">
<button class="inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#1d4ed8] bg-[#2563eb] text-white hover:bg-[#1d4ed8] aria-expanded:bg-[#1d4ed8] focus-visible:outline-[#2563eb] rounded-l-[10px] px-3.5" type="button"><svg class="size-4 flex-none" aria-hidden="true"><use href="#i-send"/></svg>Send</button><button class="group inline-flex h-10 cursor-pointer items-center gap-2 border font-semibold transition-colors duration-150 focus-visible:relative focus-visible:z-[1] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none border-[#1d4ed8] bg-[#2563eb] text-white hover:bg-[#1d4ed8] aria-expanded:bg-[#1d4ed8] focus-visible:outline-[#2563eb] -ml-px w-10 justify-center rounded-r-[10px] border-l-white/40 p-0" id="sb-c-btn" type="button" aria-label="More send options" aria-haspopup="menu" aria-expanded="false" aria-controls="sb-c"><svg class="size-4 flex-none transition-[rotate] duration-200 group-aria-expanded:rotate-180 motion-reduce:transition-none" aria-hidden="true"><use href="#i-chev-up"/></svg></button>
<div class="absolute right-0 bottom-[calc(100%+6px)] z-20 min-w-[220px] rounded-xl border border-[#e3e6ec] bg-white p-1.5 shadow-[0_1px_2px_rgba(16,24,40,.04),0_14px_34px_-10px_rgba(16,24,40,.24)] animate-menu-up motion-reduce:animate-none" role="menu" id="sb-c" aria-label="Send options" hidden>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1" aria-keyshortcuts="Shift+Meta+Enter"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-clock"/></svg><span>Schedule send…</span><kbd class="ml-auto pl-[18px] font-mono text-[.75rem] font-medium text-[#596275]" aria-hidden="true">⇧⌘↵</kbd></button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-archive"/></svg>Send and archive</button>
<button type="button" class="flex w-full cursor-pointer items-center gap-2.5 rounded-lg border-0 bg-transparent px-2.5 py-2 text-left leading-[1.4] font-medium whitespace-nowrap text-[#1b2230] no-underline hover:bg-[#f1f4f9] focus:bg-[#f1f4f9] focus:outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#2563eb]" role="menuitem" tabindex="-1"><svg class="size-4 flex-none text-[#596275]" aria-hidden="true"><use href="#i-user"/></svg>Send a test to me</button>
</div>
</div></div></div>
</div></div>
CSS
@theme {
  --animate-menu-in: menu-in .14s ease-out;
  --animate-menu-up: menu-up .14s ease-out;
  @keyframes menu-in { from { opacity: 0; translate: 0 -4px; } }
  @keyframes menu-up { from { opacity: 0; translate: 0 4px; } }
}
JavaScript
// Menu button (WAI-ARIA APG pattern). A trigger has aria-haspopup="menu", aria-expanded and aria-controls="<menu id>".
// Click, Enter, Space or ArrowDown opens the menu (ArrowUp opens it on the last item). Inside: ArrowUp/ArrowDown move,
// Home/End jump, typing a letter jumps to the next item that starts with it, Esc closes and returns focus to the trigger,
// Tab or a click outside closes. Choosing an item closes the menu; checkbox items stay open so you can tick several.
(() => {
  const OPEN = '[aria-haspopup="menu"][aria-expanded="true"]';
  const menuOf = t => document.getElementById(t.getAttribute('aria-controls'));
  const ownerOf = m => m && document.querySelector(`[aria-controls="${m.id}"][aria-expanded="true"]`);
  const itemsOf = m => [...m.querySelectorAll('[role^="menuitem"]')]
    .filter(i => i.closest('[role="menu"]') === m && i.getAttribute('aria-disabled') !== 'true');

  function close(t, focus) {
    const m = menuOf(t);
    m.querySelectorAll(OPEN).forEach(s => close(s));
    t.setAttribute('aria-expanded', 'false');
    m.hidden = true;
    if (focus) t.focus();
  }
  function open(t, at) {
    const level = t.closest('[role="menu"]');
    document.querySelectorAll(OPEN).forEach(o => { if (o !== t && o.closest('[role="menu"]') === level) close(o); });
    t.setAttribute('aria-expanded', 'true');
    menuOf(t).hidden = false;
    if (at !== undefined) itemsOf(menuOf(t)).at(at)?.focus();
  }
  const closeAll = target => document.querySelectorAll(OPEN).forEach(t => {
    if (!t.closest('[role="menu"]') && !t.contains(target) && !menuOf(t).contains(target)) close(t);
  });

  document.addEventListener('click', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t) {
      if (!t.closest('[role="menu"]') && t.getAttribute('aria-expanded') === 'true') close(t);
      else open(t, e.detail === 0 ? 0 : undefined); // keyboard clicks (Enter/Space) move focus into the menu
      return;
    }
    const item = e.target.closest('[role^="menuitem"]');
    if (item && item.getAttribute('aria-disabled') !== 'true') {
      const role = item.getAttribute('role');
      if (role === 'menuitemcheckbox') { item.setAttribute('aria-checked', item.getAttribute('aria-checked') !== 'true'); return; }
      if (role === 'menuitemradio') {
        const set = item.closest('[role="group"]') || item.closest('[role="menu"]');
        set.querySelectorAll('[role="menuitemradio"]').forEach(r => r.setAttribute('aria-checked', r === item));
        // Copy the choice onto the element that shows it, e.g. the split button's label.
        document.querySelectorAll(`[data-choice="${set.closest('[role="menu"]').id}"]`).forEach(el => {
          el.textContent = item.dataset.choiceText;
          if (item.dataset.value) el.dataset.value = item.dataset.value;
        });
      }
      let t = ownerOf(item.closest('[role="menu"]'));
      while (t && t.closest('[role="menu"]')) t = ownerOf(t.closest('[role="menu"]'));
      if (t) close(t, true);
      return;
    }
    closeAll(e.target);
  });

  document.addEventListener('keydown', e => {
    const t = e.target.closest('[aria-haspopup="menu"][aria-controls]:not([data-context])');
    if (t && !t.closest('[role="menu"]')) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); open(t, e.key === 'ArrowDown' ? 0 : -1); }
      else if (e.key === 'Escape' && t.getAttribute('aria-expanded') === 'true') close(t);
      return;
    }
    const m = e.target.closest('[role="menu"]');
    if (!m) return;
    const items = itemsOf(m), i = items.indexOf(e.target), owner = ownerOf(m);
    let next;
    switch (e.key) {
      case 'ArrowDown': next = items[(i + 1) % items.length]; break;
      case 'ArrowUp': next = items.at(i <= 0 ? -1 : i - 1); break;
      case 'Home': next = items[0]; break;
      case 'End': next = items.at(-1); break;
      case 'Escape': if (owner) { e.preventDefault(); close(owner, true); } return;
      case 'Tab': closeAll(null); return;
      case ' ': if (e.target.tagName === 'A') { e.preventDefault(); e.target.click(); } return;
      default:
        if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;
        next = [...items.slice(i + 1), ...items.slice(0, i + 1)]
          .find(x => x.textContent.trim().toLowerCase().startsWith(e.key.toLowerCase()));
    }
    e.preventDefault();
    next?.focus();
  });
})();

About this set

Three ways to pair a main action with a menu of related ones. Split button with a selectable default is a merge button: the main half runs the current choice, and the arrow opens a menu of three merge methods, each with a description, where picking one moves the tick and relabels the main button. Button group with a dropdown is an email toolbar of Archive, Report spam and Delete icon buttons joined to a Move to menu of folders. Drop-up split button in a composer sits at the bottom of a message editor, so its Send options menu opens upwards and aligns to the right. The main button and the arrow are separate buttons, so each has its own accessible name and the main action never hides behind a menu. The merge methods are menuitemradio items with aria-checked, and the choice is copied onto the main button with a few lines of script. The Bootstrap version uses .btn-group, .dropdown-toggle-split and .dropup, with data-bs-reference=”parent” so the merge menu lines up with the whole group.

What’s included

  • Split button whose menu choice relabels the main action
  • Radio items with aria-checked and a visible tick
  • Icon button group with a joined Move to dropdown
  • Drop-up variant that opens above a composer footer
  • Bootstrap .btn-group, .dropdown-toggle-split and .dropup

Accessibility

The arrow buttons are named ("Choose merge method", "More send options") and carry aria-haspopup, aria-expanded and aria-controls; icon-only toolbar buttons have aria-labels and the toolbar is a labelled group. Merge methods are menuitemradio items whose aria-checked follows the choice. Every button and item has a visible focus outline, keyboard handling follows the WAI-ARIA menu button pattern, and transitions stop under prefers-reduced-motion.

Customise it

The palette is the --sb-* custom properties on .sb-wrap, with .sb-green and .sb-blue setting the filled button colours. In Bootstrap the same properties feed the --bs-btn-* variables on .sb .btn; in Tailwind edit the hex values in the button classes.