SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/button-groups

8 CSS Button Groups

3 Pure CSS5 with JSPublished 2026-10-04Free · no sign-up

Eight ways to group buttons: an attached alignment group, an icon toolbar with arrow keys, segmented toggles, a Cancel and Save pair, pagination, a vertical action stack, filter chips and a quantity stepper.

01. Attached Button Group

ToolbarGroup

Three buttons joined into one control, with shared borders and rounded outer corners only. The group is a role="group" with a label, so screen readers announce its purpose, and adjacent borders collapse with a negative margin.

View code
<div class="bgr" role="group" aria-label="Text alignment">
  <button aria-pressed="true">Left</button><button aria-pressed="false">Center</button><button aria-pressed="false">Right</button>
</div>
.bgr { box-sizing: border-box; max-width: 100%; display: inline-flex; font: 600 14px Inter, system-ui, sans-serif; }
.bgr button { padding: 9px 16px; border: 1.5px solid #d0d5dd; background: #fff; color: #344054; font: inherit; cursor: pointer; }
.bgr button + button { margin-left: -1.5px; }
.bgr button:first-child { border-radius: 10px 0 0 10px; }
.bgr button:last-child { border-radius: 0 10px 10px 0; }
.bgr button[aria-pressed=true] { position: relative; border-color: #2563eb; background: #eff6ff; color: #1d4ed8; }
.bgr button:focus-visible { position: relative; z-index: 1; outline: 2px solid #2563eb; outline-offset: 1px; }
const bs = document.querySelectorAll('.bgr button');
bs.forEach(b => b.addEventListener('click', () => bs.forEach(x => x.setAttribute('aria-pressed', String(x === b)))));

02. Icon Toolbar Group

IconToolbar

An editor toolbar made of icon buttons in a pill, using role="toolbar" with arrow-key navigation so the group is one Tab stop. Each icon button has an aria-label, and toggle buttons such as Bold report their state with aria-pressed.

View code
<div class="tbr" role="toolbar" aria-label="Formatting">
  <button aria-label="Bold" aria-pressed="true"><b>B</b></button>
  <button aria-label="Italic" aria-pressed="false" tabindex="-1"><i>I</i></button>
  <button aria-label="Underline" aria-pressed="false" tabindex="-1"><u>U</u></button>
  <button aria-label="Insert link" tabindex="-1">&#128279;</button>
</div>
.tbr { box-sizing: border-box; max-width: 100%; display: inline-flex; gap: 2px; padding: 4px; border: 1px solid #2c3346; border-radius: 12px; background: #151a26; }
.tbr button { width: 36px; height: 36px; border: 0; border-radius: 8px; background: none; color: #e7ebf5; font: 16px Georgia, serif; cursor: pointer; }
.tbr button:hover { background: #1e2433; }
.tbr button[aria-pressed=true] { background: #2b3a6b; color: #fff; }
.tbr button:focus-visible { outline: 2px solid #9db8ff; outline-offset: -2px; }
const tb = [...document.querySelectorAll('.tbr button')];
tb.forEach((b, i) => {
  b.addEventListener('click', () => { if (b.hasAttribute('aria-pressed')) b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true')); });
  b.addEventListener('keydown', e => {
    const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; if (!d) return;
    const n = tb[(i + d + tb.length) % tb.length]; tb.forEach(x => x.tabIndex = -1); n.tabIndex = 0; n.focus();
  });
});

03. Segmented Toggle Buttons

ToggleFilter

A rounded segmented control for switching a view between Day, Week and Month. It is built from buttons with aria-pressed rather than radios, which suits toolbars where the change happens straight away, and the active segment is raised.

View code
<div class="sgb" role="group" aria-label="Range">
  <button aria-pressed="false">Day</button><button aria-pressed="true">Week</button><button aria-pressed="false">Month</button>
</div>
.sgb { box-sizing: border-box; max-width: 100%; display: inline-flex; padding: 4px; border-radius: 12px; background: #eef0f4; font: 600 14px Inter, system-ui, sans-serif; }
.sgb button { padding: 8px 18px; border: 0; border-radius: 9px; background: none; color: #475467; font: inherit; cursor: pointer; }
.sgb button[aria-pressed=true] { background: #fff; color: #101828; box-shadow: 0 1px 3px rgba(16,24,40,.18); }
.sgb button:focus-visible { outline: 2px solid #2563eb; }
const sg = document.querySelectorAll('.sgb button');
sg.forEach(b => b.addEventListener('click', () => sg.forEach(x => x.setAttribute('aria-pressed', String(x === b)))));

04. Primary and Secondary Pair

ActionsForm

The common footer pair of a quiet Cancel and a bold Save, aligned to the right. The primary action comes last in reading order to match most platforms, and both buttons have equal height so the pair looks balanced.

View code
<div class="pair"><button class="ghost">Cancel</button><button class="main">Save changes</button></div>
.pair { box-sizing: border-box; max-width: 100%; width: 360px; display: flex; justify-content: flex-end; gap: 10px; padding-top: 16px; border-top: 1px solid #262d3e; font: 700 14px Inter, system-ui, sans-serif; }
.pair button { padding: 10px 16px; border-radius: 10px; font: inherit; cursor: pointer; }
.ghost { border: 1px solid #343c51; background: none; color: #e7ebf5; }
.main { border: 0; background: #7c9cff; color: #0e1118; }
.pair button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

05. Pagination Button Group

PaginationNavigation

Page number buttons in an attached group with Previous and Next at the ends. It is a nav landmark labelled Pagination, the current page is marked with aria-current, and Previous is disabled on the first page.

View code
<nav class="pgn" aria-label="Pagination">
  <a aria-disabled="true">Previous</a><a href="#" aria-current="page">1</a><a href="#">2</a><a href="#">3</a><a href="#">Next</a>
</nav>
.pgn { box-sizing: border-box; max-width: 100%; display: inline-flex; flex-wrap: wrap; font: 600 14px Inter, system-ui, sans-serif; }
.pgn a { padding: 8px 13px; border: 1.5px solid #d0d5dd; color: #344054; text-decoration: none; }
.pgn a + a { margin-left: -1.5px; }
.pgn a:first-child { border-radius: 10px 0 0 10px; }
.pgn a:last-child { border-radius: 0 10px 10px 0; }
.pgn a:hover { background: #f9fafb; }
.pgn a[aria-current] { position: relative; border-color: #101828; background: #101828; color: #fff; }
.pgn a[aria-disabled] { color: #98a2b3; }
.pgn a:focus-visible { position: relative; z-index: 1; outline: 2px solid #2563eb; }

06. Vertical Button Stack

StackSidebar

A vertical group of actions for sidebars and side panels, with full-width buttons that share borders. A destructive action at the bottom is separated by a gap and coloured red, so it is not pressed by accident.

View code
<div class="vs" role="group" aria-label="File actions">
  <button>Rename</button><button>Duplicate</button><button>Move to folder</button>
  <button class="del">Delete file</button>
</div>
.vs { box-sizing: border-box; max-width: 100%; width: 220px; display: flex; flex-direction: column; font: 500 14px Inter, system-ui, sans-serif; }
.vs button { padding: 10px 14px; border: 1px solid #2c3346; background: #151a26; color: #e7ebf5; font: inherit; text-align: left; cursor: pointer; }
.vs button + button { margin-top: -1px; }
.vs button:first-child { border-radius: 10px 10px 0 0; }
.vs button:nth-child(3) { border-radius: 0 0 10px 10px; }
.vs .del { margin-top: 10px !important; border-radius: 10px; border-color: #7f1d1d; color: #fca5a5; }
.vs button:hover { background: #1e2433; }
.vs button:focus-visible { position: relative; outline: 2px solid #9db8ff; }

07. Filter Chip Group

FilterToggle

A wrapping group of toggle chips for quick filters, where several can be on at once. Each chip is a button with aria-pressed, a tick appears on active chips, and a Clear button resets them all in one click.

View code
<div class="fcg" role="group" aria-label="Filters">
  <button aria-pressed="true">In stock</button><button aria-pressed="false">On sale</button><button aria-pressed="true">Free shipping</button><button aria-pressed="false">New</button>
  <button class="clr">Clear</button>
</div>
.fcg { box-sizing: border-box; max-width: 100%; width: 360px; display: flex; flex-wrap: wrap; gap: 8px; font: 600 13.5px Inter, system-ui, sans-serif; }
.fcg button { padding: 7px 12px; border: 1.5px solid #d0d5dd; border-radius: 999px; background: #fff; color: #344054; font: inherit; cursor: pointer; }
.fcg button[aria-pressed=true] { border-color: #0f766e; background: #f0fdfa; color: #115e59; }
.fcg button[aria-pressed=true]::before { content: "\2713  "; }
.fcg .clr { border-color: transparent; color: #1d4ed8; text-decoration: underline; }
.fcg button:focus-visible { outline: 2px solid #0f766e; outline-offset: 2px; }
const ch = [...document.querySelectorAll('.fcg [aria-pressed]')];
ch.forEach(b => b.addEventListener('click', () => b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true'))));
document.querySelector('.fcg .clr').addEventListener('click', () => ch.forEach(b => b.setAttribute('aria-pressed', 'false')));

08. Quantity Stepper Buttons

E-commerceNumber

A minus, number and plus group for cart quantities. The buttons are named Decrease and Increase quantity, the number is a real input with min and max, and the minus button disables itself at the minimum.

View code
<div class="qty" role="group" aria-label="Quantity">
  <button aria-label="Decrease quantity" id="q-dn">&minus;</button>
  <input id="q-v" type="number" value="1" min="1" max="10" aria-label="Quantity">
  <button aria-label="Increase quantity" id="q-up">+</button>
</div>
.qty { box-sizing: border-box; max-width: 100%; display: inline-flex; overflow: hidden; border: 1px solid #343c51; border-radius: 12px; background: #151a26; }
.qty button { width: 42px; border: 0; background: none; color: #fff; font: 600 20px Inter, system-ui, sans-serif; cursor: pointer; }
.qty button:hover { background: #1e2433; }
.qty button:disabled { color: #4a5368; cursor: default; }
.qty input { width: 48px; border: 0; border-inline: 1px solid #343c51; background: none; color: #fff; font: 700 16px Inter, sans-serif; text-align: center; -moz-appearance: textfield; }
.qty input::-webkit-inner-spin-button { appearance: none; }
.qty button:focus-visible, .qty input:focus-visible { outline: 2px solid #9db8ff; outline-offset: -2px; }
const v = document.getElementById('q-v'), dn = document.getElementById('q-dn'), up = document.getElementById('q-up');
function set(n) { v.value = Math.max(1, Math.min(10, n)); dn.disabled = +v.value <= 1; up.disabled = +v.value >= 10; }
dn.addEventListener('click', () => set(+v.value - 1)); up.addEventListener('click', () => set(+v.value + 1)); v.addEventListener('change', () => set(+v.value || 1));
set(1);

About these CSS Button Groups

Name the group

Wrap related buttons in an element with role="group" or role="toolbar" and an aria-label, so screen readers announce what the buttons are for before reading them.

Toggle state

Buttons that switch something on or off use aria-pressed. Styling from that attribute keeps the look and the announced state in sync.

Toolbars and arrow keys

A toolbar is a single Tab stop: arrow keys move between its buttons, done with a roving tabindex. This keeps long toolbars from filling the Tab order.

Attached borders

Overlapping borders by a negative margin and rounding only the outer corners makes separate buttons read as one control, while each stays its own focusable button.

Frequently asked questions

Group or segmented control?

Use aria-pressed buttons when the change happens at once; use radio buttons inside a form.

Which demos need JavaScript?

Most, to toggle aria-pressed. The pair and vertical stack are pure CSS.

How do I keep focus rings visible on attached buttons?

Raise the focused button with position: relative and a z-index, as these demos do.