Javascript is required
The whole idea of Toggle button group is that it relies on state change. Therefore Javascript is needed for it to work as intended.
Components
Groups related buttons by wrapping them with <yourElement class="toggle-button-group" role="group">
.
The whole idea of Toggle button group is that it relies on state change. Therefore Javascript is needed for it to work as intended.
If you just need to group a bunch of "dumb" (uncontrolled) buttons - use Button group.
If your buttons depend on state (controlled) - use Toggle button group.
<div role="group" class="toggle-button-group">
<button class="selected">
<svg></svg>
Selected
</button>
<button>Enabled</button>
<button disabled>Disabled</button>
</div>
Try playing with this example. Notice how the .selected
icon replaces the pre-existing one.
Only show two things at once: icon + text or icon + icon.
<div role="group" class="toggle-button-group">
<button class="selected">
<svg><!-- Selected icon (checkmark) --></svg>
Label
</button>
</div>
Don't forget to use aria-label
on the <button>
element whenever you don't have another way to label your button.
Icons can be used in place of labels, but they must clearly communicate their meaning.
<div role="group" class="toggle-button-group">
<button class="selected" aria-label="Walking">
<svg><!-- Selected icon (checkmark) --></svg>
<svg><!-- Main icon --></svg>
</button>
<button aria-label="Cycling">
<svg><!--Main icon --></svg>
</button>
</div>
Avoid mixing icon-only buttons with text buttons. Choose one type and use that type for all segments.
Choose between three sizes: default, .small
and .x-small
.
<div role="group" class="toggle-button-group">
<!-- -->
</div>
<div role="group" class="toggle-button-group small">
<!-- -->
</div>
<div role="group" class="toggle-button-group x-small">
<!-- -->
</div>
<div role="group" class="toggle-button-group fullwidth">
<!-- -->
</div>
You probably don't need that.
Vertical button groups are largely a legacy design pattern that can be better handled through:
Horizontal groups or alternative patterns altogether usually provide better UX.
Create an issue or a PR and let me know!
<element role="group" class="toggle-button-group">
<button>
elements.selected
checkmark, text label or iconType | Modifiers | Default | Description |
---|---|---|---|
Button children | & > button > svg/label , & > button.selected > svg + svg/label | - | Child content the buttons support. |
Button size | .dynamic | - | If enabled all button widths are made to fit their respective content. |
Sizes | default, .small , .x-small , | default | The size to use. |
@layer components.base {
:where([role="group"].toggle-button-group) {
--_border-radius: var(--radius-round);
--_button-padding-inline: clamp(var(--size-2), 3cqi, var(--size-4));
--_max-width: auto;
--_icon-size: var(--size-4);
background-color: var(--surface-default);
border: 1px solid var(--border-color);
border-radius: var(--_border-radius);
display: flex;
grid-auto-columns: 1fr;
grid-auto-flow: column;
max-inline-size: var(--_max-width);
min-inline-size: max-content;
overflow: clip;
/* Size */
&.small {
button {
min-block-size: 2.1875rem; /* 35px */
}
}
&.x-small {
button {
min-block-size: var(--size-6); /* 30px */
}
}
&.fullwidth {
inline-size: 100%;
}
/* Button */
button {
--_bg-color: transparent;
align-items: center;
background: var(--_bg-color) var(--ripple, none);
border-radius: 0;
border-inline: 1px solid var(--border-color);
border-inline-start-width: 0;
color: var(--text-color-1);
display: inline-flex;
flex: auto;
gap: 1ex;
justify-content: center;
min-block-size: 2.5rem; /* 40px */
min-inline-size: 5ex;
outline-offset: calc(-1 * var(--size-2));
padding: 0 var(--_button-padding-inline);
position: relative;
user-select: none;
@media (prefers-reduced-motion: no-preference) {
transition:
background-color 0.2s var(--ease-out-3),
box-shadow 0.2s var(--ease-out-3),
border-color 0.2s var(--ease-out-3),
color 0.2s var(--ease-out-3),
outline-offset 0.05s var(--ease-1);
}
/* Element states */
&:hover {
--_bg-color: light-dark(oklch(0% 0 0 / 0.04), oklch(100% 0 0 / 0.08));
}
&:focus-visible {
outline-offset: -6px;
}
/* Disabled */
&[disabled] {
border-color: color-mix(in oklch, var(--border-color) 50%, transparent);
cursor: not-allowed;
color: color-mix(in oklch, var(--text-color-1) 30%, transparent);
}
&[disabled] + &:not([disabled]):not(:last-of-type) {
border-inline-end-width: 1px;
}
/* Assign border radius for outline */
&:first-of-type {
border-bottom-left-radius: var(--_border-radius);
border-top-left-radius: var(--_border-radius);
}
&:last-of-type {
border-bottom-right-radius: var(--_border-radius);
border-top-right-radius: var(--_border-radius);
}
&:last-of-type {
border-inline-end-width: 0;
}
/* Ripple effect */
background-position: center;
&:where(:not([disabled])) {
&:where(:not(:active):hover) {
--ripple: radial-gradient(circle, transparent 1%, var(--_bg-color) 1%)
center/15000%;
transition: background var(--button-ripple-duration);
}
&:where(:hover:active) {
background-size: var(--button-ripple-size);
transition: background 0s;
}
}
/* Icons */
svg {
inline-size: var(--_icon-size);
inset-inline-start: calc(var(--_button-padding-inline));
& + svg {
max-block-size: var(--size-5);
max-inline-size: var(--size-7);
}
}
/* Selected */
&.selected {
--_bg-color: color-mix(
in oklch,
var(--primary) 12%,
var(--surface-default)
);
/* Checkmark */
svg:first-of-type {
margin-block-end: -3px;
}
}
}
}
}