Split button
Category: Buttons · Tags: split-button, menu, dropdown, actions, menu-button
How to use it
Use when one action is by far the most common and two or three close variants of it are occasionally needed, such as Publish now versus Schedule or Save as draft. Do not hide unrelated or destructive actions in the menu, and if no option is clearly the default, use a plain menu button or separate buttons. After copying, change the ids (the arrow's aria-controls must match the menu id), the labels, and the action each item performs.
Accessibility
The main action is a plain <button>; the arrow next to it is a separate <button> with aria-label="More publish options", aria-haspopup="menu", aria-controls and aria-expanded. The list follows the WAI-ARIA menu pattern: role="menu" with role="menuitem" buttons, the first item takes focus when it opens (Enter, Space or ArrowDown), ArrowUp/ArrowDown/Home/End move between items, Escape closes it and returns focus to the arrow button, and Tab or a click outside closes it. A role="status" line confirms which action ran, and every button has a 2px focus outline. Text is #ffffff on #1d4ed8 (6.70:1) and on the #1e40af hover (8.72:1); menu items are #0f172a on #ffffff (17.85:1) and on the #e2e8f0 hover (14.48:1).
Source (copy-ready)
<div class="dcl-splitbtn">
<div class="dcl-splitbtn__group">
<button type="button" class="dcl-splitbtn__main" data-action="Published now">Publish</button>
<button type="button" class="dcl-splitbtn__toggle" aria-label="More publish options" aria-haspopup="menu" aria-expanded="false" aria-controls="dcl-splitbtn-menu">
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false"><path d="M3.5 6l4.5 4.5L12.5 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="dcl-splitbtn__menu" id="dcl-splitbtn-menu" role="menu" aria-label="Publish options" hidden>
<li class="dcl-splitbtn__entry" role="none"><button type="button" role="menuitem" tabindex="-1" class="dcl-splitbtn__item" data-action="Scheduled for 9:00 tomorrow">Schedule for tomorrow</button></li>
<li class="dcl-splitbtn__entry" role="none"><button type="button" role="menuitem" tabindex="-1" class="dcl-splitbtn__item" data-action="Saved as draft">Save as draft</button></li>
</ul>
</div>
<p class="dcl-splitbtn__status" role="status"></p>
</div>
<style>
.dcl-splitbtn { font: inherit; max-width: 100%; color: #0f172a; background-color: #ffffff; }
.dcl-splitbtn__group { position: relative; display: inline-flex; max-width: 100%; }
.dcl-splitbtn__main, .dcl-splitbtn__toggle { display: inline-flex; align-items: center; min-height: 2.75rem; border: none; cursor: pointer; font: inherit; font-weight: 600; color: #ffffff; background-color: #1d4ed8; }
.dcl-splitbtn__main { padding: 0 1.1rem; border-radius: 0.5rem 0 0 0.5rem; }
.dcl-splitbtn__toggle { padding: 0 0.7rem; border-left: 1px solid #bfdbfe; border-radius: 0 0.5rem 0.5rem 0; }
.dcl-splitbtn__main:hover, .dcl-splitbtn__toggle:hover { color: #ffffff; background-color: #1e40af; }
.dcl-splitbtn__main:focus-visible, .dcl-splitbtn__toggle:focus-visible { outline: 2px solid #0f172a; outline-offset: 2px; }
.dcl-splitbtn__menu { position: absolute; top: 100%; right: 0; z-index: 2; display: block; min-width: 12rem; max-width: calc(100vw - 2rem); margin: 0.25rem 0 0; padding: 0.25rem; list-style: none; border: 1px solid #94a3b8; border-radius: 0.5rem; color: #0f172a; background-color: #ffffff; }
.dcl-splitbtn__menu[hidden] { display: none; }
.dcl-splitbtn__entry { margin: 0; }
.dcl-splitbtn__item { display: block; width: 100%; padding: 0.6rem 0.75rem; border: none; border-radius: 0.25rem; text-align: left; cursor: pointer; font: inherit; color: #0f172a; background-color: #ffffff; }
.dcl-splitbtn__item:hover { color: #0f172a; background-color: #e2e8f0; }
.dcl-splitbtn__item:focus-visible { outline: 2px solid #1d4ed8; outline-offset: -2px; }
.dcl-splitbtn__status { min-height: 1.5em; margin: 0.5rem 0 0; color: #0f172a; background-color: #ffffff; }
</style>
<script>
(function () {
document.querySelectorAll(".dcl-splitbtn").forEach(function (root) {
if (root.dataset.dclBound) { return; }
root.dataset.dclBound = "true";
var main = root.querySelector(".dcl-splitbtn__main");
var toggle = root.querySelector(".dcl-splitbtn__toggle");
var menu = root.querySelector(".dcl-splitbtn__menu");
var status = root.querySelector(".dcl-splitbtn__status");
if (!toggle || !menu) { return; }
var items = Array.prototype.slice.call(menu.querySelectorAll("[role=\"menuitem\"]"));
function report(button) {
if (status && button) { status.textContent = button.getAttribute("data-action") || ""; }
}
function open() {
menu.hidden = false;
toggle.setAttribute("aria-expanded", "true");
if (items[0]) { items[0].focus(); }
}
function close(returnFocus) {
menu.hidden = true;
toggle.setAttribute("aria-expanded", "false");
if (returnFocus) { toggle.focus(); }
}
toggle.addEventListener("click", function () {
if (menu.hidden) { open(); } else { close(false); }
});
toggle.addEventListener("keydown", function (event) {
if (event.key === "ArrowDown") { event.preventDefault(); open(); }
});
if (main) { main.addEventListener("click", function () { report(main); }); }
items.forEach(function (item, index) {
item.addEventListener("click", function () { report(item); close(true); });
item.addEventListener("keydown", function (event) {
var next = null;
if (event.key === "ArrowDown") { next = items[(index + 1) % items.length]; }
if (event.key === "ArrowUp") { next = items[(index - 1 + items.length) % items.length]; }
if (event.key === "Home") { next = items[0]; }
if (event.key === "End") { next = items[items.length - 1]; }
if (next) { event.preventDefault(); next.focus(); }
if (event.key === "Escape") { event.preventDefault(); close(true); }
if (event.key === "Tab") { close(false); }
});
});
document.addEventListener("click", function (event) {
if (!menu.hidden && !root.contains(event.target)) { close(false); }
});
});
})();
</script>
Was this useful? Send feedback.