Split button

스플릿 버튼

A two-part button — the left side runs a default action immediately, the right-side arrow opens a menu of alternatives.

Also known as: Split dropdownCombo buttonDropdown split button
···
html
<div class="sbw">
  <div class="sbgroup" role="group" aria-label="내보내기">
    <button class="sbmain" id="sbmain">PDF로 내보내기</button>
    <button class="sbcaret" id="sbcaret" aria-haspopup="menu" aria-expanded="false" aria-label="다른 형식으로 내보내기">▾</button>
    <ul class="sbmenu" role="menu" id="sbmenu">
      <li role="menuitem" tabindex="-1">CSV로 내보내기</li>
      <li role="menuitem" tabindex="-1">PNG로 내보내기</li>
      <li role="menuitem" tabindex="-1">링크 복사</li>
    </ul>
  </div>
  <p class="sbstatus" id="sbstatus">왼쪽을 누르면 바로 PDF가 저장돼요</p>
</div>
css
.sbw{position:absolute;inset:0;display:grid;place-items:center;gap:14px}
.sbgroup{display:flex;position:relative}
.sbmain{padding:9px 14px;border:1px solid var(--accent);border-right:0;border-radius:9px 0 0 9px;background:var(--accent);color:#fff;font-size:12px;font-weight:700;cursor:default}
.sbcaret{padding:9px 10px;border:1px solid var(--accent);border-left:1px solid rgba(255,255,255,.4);border-radius:0 9px 9px 0;background:var(--accent);color:#fff;font-size:11px;cursor:default}
.sbmenu{position:absolute;top:calc(100% + 6px);left:0;list-style:none;margin:0;padding:5px;width:150px;background:var(--surface);border:1px solid var(--line);border-radius:10px;box-shadow:0 14px 30px rgba(0,0,0,.2);opacity:0;pointer-events:none;transform:translateY(4px);transition:opacity .16s,transform .16s}
.sbmenu[data-open]{opacity:1;pointer-events:auto;transform:translateY(0)}
.sbmenu li{padding:7px 9px;border-radius:6px;font-size:11px;color:var(--fg)}
.sbmenu li[data-hi]{background:var(--accent);color:#fff}
.sbstatus{position:absolute;bottom:10px;font-size:10px;color:var(--muted);text-align:center;width:80%}
js
const main = document.getElementById('sbmain'), caret = document.getElementById('sbcaret'), menu = document.getElementById('sbmenu'), status = document.getElementById('sbstatus');
const items = [...menu.querySelectorAll('li')];
function open(v) { menu.toggleAttribute('data-open', v); caret.setAttribute('aria-expanded', String(v)); }
let auto = true;
main.addEventListener('click', () => { auto = false; status.textContent = 'PDF 저장됨'; });
caret.addEventListener('click', () => { auto = false; open(menu.getAttribute('data-open') === null); });
document.addEventListener('click', (e) => { if (!menu.contains(e.target) && e.target !== caret) open(false); });
function loop() {
  if (!auto) return;
  open(true);
  let i = 0;
  const hi = setInterval(() => {
    if (!auto) return clearInterval(hi);
    items.forEach((it, idx) => it.toggleAttribute('data-hi', idx === i));
    i++;
    if (i > items.length) { clearInterval(hi); setTimeout(() => { if (auto) { open(false); setTimeout(loop, 1000); } }, 300); }
  }, 380);
}
setTimeout(loop, 500);

The two halves sit flush together visually but are actually two separate <button> elements, so Tab focuses them independently, and only the arrow button carries aria-haspopup="menu" and aria-expanded.

Easy to confuse with a plain dropdown button, but the behavior differs. A dropdown button's whole surface is just a trigger — pressing any part of it opens the menu and nothing runs on its own. A split button's left side runs the most common action immediately ("Export as PDF," say), and only pressing the right arrow reveals the remaining alternatives (CSV, PNG, Copy link) as a menu.

Keyboard behavior once the menu is open — arrow keys to move, Enter to run, Esc to close — matches a dropdown menu exactly.