스플릿 버튼

Split button

왼쪽은 즉시 실행되는 기본 동작, 오른쪽은 대안 동작 목록을 펼치는 화살표로 나뉜 두 부분짜리 버튼.

다른 이름: 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);

두 영역은 시각적으로 붙어 있지만 실제로는 서로 다른 두 개의 <button>이라 Tab으로 각각 따로 포커스를 받고, 화살표 버튼에만 aria-haspopup="menu"와 aria-expanded를 둡니다.

'드롭다운 버튼'과 헷갈리기 쉽지만 동작이 다릅니다. 드롭다운 버튼은 버튼 전체가 트리거일 뿐 그 자체로는 아무 동작도 실행하지 않고 항상 메뉴부터 엽니다. 스플릿 버튼은 왼쪽을 누르면 가장 흔히 쓰는 동작(예: "PDF로 내보내기")이 즉시 실행되고, 오른쪽 화살표를 눌렀을 때만 나머지 대안(CSV, PNG, 링크 복사)이 메뉴로 펼쳐집니다.

메뉴가 열렸을 때의 키보드 동작(방향키로 이동, Enter로 실행, Esc로 닫기)은 드롭다운 메뉴와 동일합니다.