Skip to main content

Theme config

Theme mode

Color palette

Grays

Border radii/radiuses/radiopedes/you know
Border radius
Field border radius
Button border radius
All
Components
Guides
API
Recent

Components

Button group

Groups related buttons.

Full support Supported since v111. Full support Supported since v113. Full support Supported since v16.2.
  • Button groups should consist of 2-5 buttons.
  • Don't allow them to wrap onto a new line.
  • If an icon is used without label text make sure the button communicates clearly what it does.
Button group or Toggle group?

If your buttons depend on state (controlled) - use Toggle group.

If you just need to group a bunch of "dumb" (uncontrolled) buttons - use Button group.

Variants

Change the appearance of the entire group with the variant prop.

---
import { ButtonGroup } from "@opui/astro"
import { Button } from "@opui/astro"
---
<ButtonGroup>
<Button>Text</Button>
<Button>Text</Button>
<Button>Text</Button>
</ButtonGroup>
<ButtonGroup variant="outlined">
<Button>Outlined</Button>
<Button>Outlined</Button>
<Button>Outlined</Button>
</ButtonGroup>
<ButtonGroup variant="tonal">
<Button>Tonal</Button>
<Button>Tonal</Button>
<Button>Tonal</Button>
</ButtonGroup>
<ButtonGroup variant="filled">
<Button>Filled</Button>
<Button>Filled</Button>
<Button>Filled</Button>
</ButtonGroup>
<div class="button-group" role="group">
<button class="button">Text</button><button class="button">Text</button
><button class="button">Text</button>
</div>
<div class="button-group outlined" role="group">
<button class="button">Outlined</button
><button class="button">Outlined</button
><button class="button">Outlined</button>
</div>
<div class="button-group tonal" role="group">
<button class="button">Tonal</button><button class="button">Tonal</button
><button class="button">Tonal</button>
</div>
<div class="button-group filled" role="group">
<button class="button">Filled</button><button class="button">Filled</button
><button class="button">Filled</button>
</div>

Colors

Set the color prop to primary or critical to recolor the entire group. The default is a neutral gray.

---
import { ButtonGroup } from "@opui/astro"
import { Button } from "@opui/astro"
---
<ButtonGroup color="primary" variant="filled">
<Button>Primary</Button>
<Button>Primary</Button>
<Button>Primary</Button>
</ButtonGroup>
<ButtonGroup color="critical" variant="filled">
<Button>Critical</Button>
<Button>Critical</Button>
<Button>Critical</Button>
</ButtonGroup>
<div class="button-group primary filled" role="group">
<button class="button">Primary</button><button class="button">Primary</button
><button class="button">Primary</button>
</div>
<div class="button-group critical filled" role="group">
<button class="button">Critical</button
><button class="button">Critical</button
><button class="button">Critical</button>
</div>

Icons

Yes of course, they're just buttons.

---
import { ButtonGroup } from "@opui/astro"
import { Button } from "@opui/astro"
const checkIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M29.907 5.14a1.25 1.25 0 0 1-.047 1.767l-19 18a1.25 1.25 0 0 1-1.775-.055l-6.75-7.25a1.25 1.25 0 0 1 1.83-1.704l5.89 6.327L28.14 5.093a1.25 1.25 0 0 1 1.767.047"></path></svg>`
const helpIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M11.499 10.803A4.505 4.505 0 0 1 16 6.5a4.5 4.5 0 0 1 4.5 4.5c0 1.276-.298 2.02-.676 2.565c-.368.53-.836.925-1.471 1.459l-.286.241c-.745.632-1.614 1.42-2.268 2.628c-.659 1.217-1.049 2.76-1.049 4.857a1.25 1.25 0 0 0 2.5 0c0-1.778.328-2.892.748-3.667c.424-.783.993-1.324 1.685-1.91l.259-.217c.616-.515 1.363-1.139 1.937-1.966C22.579 13.98 23 12.724 23 11a7 7 0 0 0-7-7a7.005 7.005 0 0 0-6.999 6.695a1.25 1.25 0 1 0 2.498.108M16 29a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"></path></svg>`
const closeIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M26.29 4.293a1 1 0 1 1 1.414 1.414L17.413 16l10.291 10.29a1 1 0 1 1-1.414 1.414L16 17.413L5.707 27.704a1 1 0 0 1-1.414-1.414L14.585 16L4.293 5.707a1 1 0 0 1 1.414-1.414L16 14.584z"></path></svg>`
---
<ButtonGroup variant="outlined">
<Button aria-label="Label">
<Fragment set:html={checkIcon} />
</Button>
<Button aria-label="Label"> Maybe </Button>
<Button aria-label="Label">
<Fragment set:html={closeIcon} />
</Button>
</ButtonGroup>
<ButtonGroup variant="outlined">
<Button>
<Fragment set:html={checkIcon} />
OK
</Button>
<Button>
<Fragment set:html={helpIcon} />
Maybe
</Button>
<Button>
<Fragment set:html={closeIcon} />
No
</Button>
</ButtonGroup>
<div class="button-group outlined" role="group">
<button aria-label="Label" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M29.907 5.14a1.25 1.25 0 0 1-.047 1.767l-19 18a1.25 1.25 0 0 1-1.775-.055l-6.75-7.25a1.25 1.25 0 0 1 1.83-1.704l5.89 6.327L28.14 5.093a1.25 1.25 0 0 1 1.767.047"
></path>
</svg></button
><button aria-label="Label" class="button">Maybe</button
><button aria-label="Label" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M26.29 4.293a1 1 0 1 1 1.414 1.414L17.413 16l10.291 10.29a1 1 0 1 1-1.414 1.414L16 17.413L5.707 27.704a1 1 0 0 1-1.414-1.414L14.585 16L4.293 5.707a1 1 0 0 1 1.414-1.414L16 14.584z"
></path>
</svg>
</button>
</div>
<div class="button-group outlined" role="group">
<button class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M29.907 5.14a1.25 1.25 0 0 1-.047 1.767l-19 18a1.25 1.25 0 0 1-1.775-.055l-6.75-7.25a1.25 1.25 0 0 1 1.83-1.704l5.89 6.327L28.14 5.093a1.25 1.25 0 0 1 1.767.047"
></path>
</svg>
OK</button
><button class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M11.499 10.803A4.505 4.505 0 0 1 16 6.5a4.5 4.5 0 0 1 4.5 4.5c0 1.276-.298 2.02-.676 2.565c-.368.53-.836.925-1.471 1.459l-.286.241c-.745.632-1.614 1.42-2.268 2.628c-.659 1.217-1.049 2.76-1.049 4.857a1.25 1.25 0 0 0 2.5 0c0-1.778.328-2.892.748-3.667c.424-.783.993-1.324 1.685-1.91l.259-.217c.616-.515 1.363-1.139 1.937-1.966C22.579 13.98 23 12.724 23 11a7 7 0 0 0-7-7a7.005 7.005 0 0 0-6.999 6.695a1.25 1.25 0 1 0 2.498.108M16 29a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"
></path>
</svg>
Maybe</button
><button class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M26.29 4.293a1 1 0 1 1 1.414 1.414L17.413 16l10.291 10.29a1 1 0 1 1-1.414 1.414L16 17.413L5.707 27.704a1 1 0 0 1-1.414-1.414L14.585 16L4.293 5.707a1 1 0 0 1 1.414-1.414L16 14.584z"
></path>
</svg>
No
</button>
</div>

Sizes

Adjust the size of all buttons in the group using the size prop.

---
import { ButtonGroup } from "@opui/astro"
import { Button } from "@opui/astro"
---
<ButtonGroup size="small" variant="outlined">
<Button>Small</Button>
<Button>Small</Button>
<Button>Small</Button>
</ButtonGroup>
<ButtonGroup variant="outlined">
<Button>Default</Button>
<Button>Default</Button>
<Button>Default</Button>
</ButtonGroup>
<ButtonGroup size="large" variant="outlined">
<Button>Large</Button>
<Button>Large</Button>
<Button>Large</Button>
</ButtonGroup>
<div class="button-group small outlined" role="group">
<button class="button">Small</button><button class="button">Small</button
><button class="button">Small</button>
</div>
<div class="button-group outlined" role="group">
<button class="button">Default</button><button class="button">Default</button
><button class="button">Default</button>
</div>
<div class="button-group large outlined" role="group">
<button class="button">Large</button><button class="button">Large</button
><button class="button">Large</button>
</div>

Vertical orientation

Change the layout of the group with the orientation="vertical" prop.

---
import { ButtonGroup } from "@opui/astro"
import { Button } from "@opui/astro"
const plusIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`
const minusIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`
---
<div class="example-row">
<ButtonGroup orientation="vertical">
<Button aria-label="Up">
<Fragment set:html={plusIcon} />
</Button>
<Button aria-label="Decrease">
<Fragment set:html={minusIcon} />
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="outlined">
<Button aria-label="Up">
<Fragment set:html={plusIcon} />
</Button>
<Button aria-label="Decrease">
<Fragment set:html={minusIcon} />
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="tonal">
<Button aria-label="Up">
<Fragment set:html={plusIcon} />
</Button>
<Button aria-label="Decrease">
<Fragment set:html={minusIcon} />
</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="filled">
<Button aria-label="Up">
<Fragment set:html={plusIcon} />
</Button>
<Button aria-label="Decrease">
<Fragment set:html={minusIcon} />
</Button>
</ButtonGroup>
</div>
<div class="example-row">
<ButtonGroup orientation="vertical">
<Button>Up</Button>
<Button>Down</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="outlined">
<Button>Up</Button>
<Button>Down</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="tonal">
<Button>Up</Button>
<Button>Down</Button>
</ButtonGroup>
<ButtonGroup orientation="vertical" variant="filled">
<Button>Up</Button>
<Button>Down</Button>
</ButtonGroup>
</div>
<div class="example-row">
<div class="button-group vertical" role="group">
<button aria-label="Up" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg></button
><button aria-label="Decrease" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="button-group outlined vertical" role="group">
<button aria-label="Up" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg></button
><button aria-label="Decrease" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="button-group tonal vertical" role="group">
<button aria-label="Up" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg></button
><button aria-label="Decrease" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="button-group filled vertical" role="group">
<button aria-label="Up" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg></button
><button aria-label="Decrease" class="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
</div>
<div class="example-row">
<div class="button-group vertical" role="group">
<button class="button">Up</button><button class="button">Down</button>
</div>
<div class="button-group outlined vertical" role="group">
<button class="button">Up</button><button class="button">Down</button>
</div>
<div class="button-group tonal vertical" role="group">
<button class="button">Up</button><button class="button">Down</button>
</div>
<div class="button-group filled vertical" role="group">
<button class="button">Up</button><button class="button">Down</button>
</div>
</div>

Disabled

Disable individual buttons within a group by setting the disabled prop on each Button.

---
import { ButtonGroup } from "@opui/astro"
import { Button } from "@opui/astro"
---
<ButtonGroup variant="filled">
<Button>Enabled</Button>
<Button disabled>Disabled</Button>
<Button>Enabled</Button>
</ButtonGroup>
<ButtonGroup variant="filled" color="primary">
<Button>Enabled</Button>
<Button disabled>Disabled</Button>
<Button>Enabled</Button>
</ButtonGroup>
<div class="button-group filled" role="group">
<button class="button">Enabled</button
><button disabled class="button disabled">Disabled</button
><button class="button">Enabled</button>
</div>
<div class="button-group primary filled" role="group">
<button class="button">Enabled</button
><button disabled class="button disabled">Disabled</button
><button class="button">Enabled</button>
</div>

Anatomy

  1. Container: <element role="group" class="button-group">
  2. Buttons: Button

API

Button group

Prop Type Default Description
color "critical", "primary" - Color of every button in the group. Default is a neutral gray.
orientation "vertical" - The layout orientation of the group.
size "small" | "large" - The size of the buttons in the group.
variant "outlined" | "tonal" | "filled" - The visual style of the buttons in the group.

Button

Prop Type Default Description
size "small", "large" - The size of the button.
variant "outlined", "tonal", "filled" - The visual variant of the button.
color "critical", "primary" - The color of the button. Default is a neutral gray.
href string - Renders as an tag if an href is provided.
disabled boolean - Button disabled state.

Browser support

Full support Supported since v111. Full support Supported since v113. Full support Supported since v16.2.

See also the full browser support guide.

Installation

@layer components.extended {
:where([role="group"].button-group) {
--_border-radius: var(--button-border-radius);
border-radius: var(--_border-radius);
display: inline-flex;
min-width: max-content;
/* Button */
button {
border-radius: 0;
/* button.css > --_accent-tonal */
--_divider-color: light-dark(oklch(from var(--_accent-tonal) calc(l - 0.1) c h),
oklch(from var(--_accent-tonal) calc(l + 0.1) c h));
&.outlined {
--_divider-color: var(--_border-color);
}
&.filled {
--_divider-color: currentColor;
}
&:focus-visible {
outline-offset: -4px;
}
/* Color inheritance from Button Group — shared interaction model.
The .critical scope class in theme.css redirects --palette-source so
--color-6 below becomes a tint of the active color. */
:where(.button-group.critical,
.button-group.primary) & {
--_color-contrast: var(--gray-1);
--_color-tonal: var(--color-6);
--_accent-tonal-contrast: var(--text-primary-contrast);
--_default-hover-bg-color: oklch(from var(--_accent) l c h / 15%);
--_default-active-bg-color: oklch(from var(--_accent) l c h / 25%);
--_filled-hover-bg-color: oklch(from var(--_accent) calc(l - 0.1) c h);
--_filled-active-bg-color: oklch(from var(--_accent) calc(l - 0.15) c h);
--_outlined-active-bg-color: oklch(from var(--_accent) calc(l - 0.1) c h);
}
:where(.button-group.critical) & {
--_color: var(--critical);
}
:where(.button-group.primary) & {
--_color: var(--primary);
}
:where(.button-group.small) & {
--_min-height: 1.875rem;
padding-block: 0;
padding-inline: 1ex;
}
:where(.button-group.large) & {
--_min-height: 2.875rem;
padding-inline: 4ex;
}
:where(.button-group.outlined) & {
--_border-color: var(--_accent);
--_divider-color: var(--_accent);
&:where(:not([disabled])) {
&:where(:not(:active):hover) {
--_bg-color: var(--_accent);
--_border-color: var(--_accent);
--_text-color: var(--_accent-contrast);
}
&:where(:active) {
--_bg-color: var(--_outlined-active-bg-color);
--_border-color: var(--_outlined-active-bg-color);
--_text-color: var(--_accent-contrast);
}
}
}
:where(.button-group.tonal) & {
--_bg-color: var(--_accent-tonal);
--_text-color: var(--_accent-tonal-contrast);
&:where(:not([disabled])) {
&:where(:not(:active):hover) {
--_bg-color: var(--_accent);
--_text-color: var(--_accent-contrast);
}
&:where(:active) {
--_bg-color: var(--_outlined-active-bg-color);
--_text-color: var(--_accent-contrast);
}
}
}
:where(.button-group.filled) & {
--_bg-color: var(--_accent);
--_text-color: var(--_accent-contrast);
--_divider-color: currentColor;
:where(.button-group.critical,
.button-group.primary) & {
--_divider-color: light-dark(oklch(from var(--_accent) calc(l - 0.1) c h),
oklch(from var(--_accent) calc(l + 0.1) c h));
}
&:where(:not([disabled])) {
&:where(:not(:active):hover) {
--_bg-color: var(--_filled-hover-bg-color);
}
&:where(:active) {
--_bg-color: var(--_filled-active-bg-color);
}
}
}
}
&:not(.vertical) {
button {
&:first-of-type {
border-end-start-radius: var(--_border-radius);
border-start-start-radius: var(--_border-radius);
}
&:last-of-type {
border-end-end-radius: var(--_border-radius);
border-start-end-radius: var(--_border-radius);
}
/* Siblings */
&+& {
border-inline-start-color: var(--_divider-color);
border-inline-width: 1px;
margin-inline-start: -1px;
&[disabled] {
border-inline-start-color: color-mix(in oklch, var(--_divider-color) 40%, transparent);
}
}
}
}
&.vertical {
flex-direction: column;
button {
padding: var(--size-2);
&:has(svg) {
aspect-ratio: 1;
padding: var(--size-1);
}
&:first-of-type {
border-start-start-radius: var(--_border-radius);
border-start-end-radius: var(--_border-radius);
}
&:last-of-type {
border-end-start-radius: var(--_border-radius);
border-end-end-radius: var(--_border-radius);
}
&+& {
border-block-start: 1px solid var(--_divider-color);
margin-block-start: -1px;
&[disabled] {
border-block-start-color: color-mix(in oklch, var(--_divider-color) 40%, transparent);
}
}
}
}
}
}