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.
<script setup lang="ts">import { Button, ButtonGroup } from "opui-css/vue"</script>
<template> <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></template><!--[--><div class="ui-button-group" role="group"> <!--[--><button class="ui-button"><!--[-->Text<!--]--></button ><button class="ui-button"><!--[-->Text<!--]--></button ><button class="ui-button"><!--[-->Text<!--]--></button ><!--]--></div><div class="ui-button-group ui-outlined" role="group"> <!--[--><button class="ui-button"><!--[-->Outlined<!--]--></button ><button class="ui-button"><!--[-->Outlined<!--]--></button ><button class="ui-button"><!--[-->Outlined<!--]--></button ><!--]--></div><div class="ui-button-group ui-tonal" role="group"> <!--[--><button class="ui-button"><!--[-->Tonal<!--]--></button ><button class="ui-button"><!--[-->Tonal<!--]--></button ><button class="ui-button"><!--[-->Tonal<!--]--></button ><!--]--></div><div class="ui-button-group ui-filled" role="group"> <!--[--><button class="ui-button"><!--[-->Filled<!--]--></button ><button class="ui-button"><!--[-->Filled<!--]--></button ><button class="ui-button"><!--[-->Filled<!--]--></button ><!--]--></div><!--]-->Colors
Set the color prop to primary or critical to recolor the entire group. The default is a neutral gray.
<script setup lang="ts">import { Button, ButtonGroup } from "opui-css/vue"</script>
<template> <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></template><!--[--><div class="ui-button-group ui-primary ui-filled" role="group"> <!--[--><button class="ui-button"><!--[-->Primary<!--]--></button ><button class="ui-button"><!--[-->Primary<!--]--></button ><button class="ui-button"><!--[-->Primary<!--]--></button ><!--]--></div><div class="ui-button-group ui-critical ui-filled" role="group"> <!--[--><button class="ui-button"><!--[-->Critical<!--]--></button ><button class="ui-button"><!--[-->Critical<!--]--></button ><button class="ui-button"><!--[-->Critical<!--]--></button ><!--]--></div><!--]-->Icons
Yes of course, they're just buttons.
<script setup lang="ts">import { Button, ButtonGroup } from "opui-css/vue"
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>`</script>
<template> <ButtonGroup variant="outlined"> <Button aria-label="Label"> <span v-html="checkIcon"></span> </Button> <Button aria-label="Label"> Maybe </Button> <Button aria-label="Label"> <span v-html="closeIcon"></span> </Button> </ButtonGroup>
<ButtonGroup variant="outlined"> <Button> <span v-html="checkIcon"></span> OK </Button> <Button> <span v-html="helpIcon"></span> Maybe </Button> <Button> <span v-html="closeIcon"></span> No </Button> </ButtonGroup></template><!--[--><div class="ui-button-group ui-outlined" role="group"> <!--[--><button class="ui-button" aria-label="Label"> <!--[--><span ><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></span ><!--]--></button ><button class="ui-button" aria-label="Label"> <!--[--> Maybe <!--]--></button ><button class="ui-button" aria-label="Label"> <!--[--><span ><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></span ><!--]--></button ><!--]--></div><div class="ui-button-group ui-outlined" role="group"> <!--[--><button class="ui-button"> <!--[--><span ><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 ></span> OK <!--]--></button ><button class="ui-button"> <!--[--><span ><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 ></span> Maybe <!--]--></button ><button class="ui-button"> <!--[--><span ><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 ></span> No <!--]--></button ><!--]--></div><!--]-->Sizes
Adjust the size of all buttons in the group using the size
prop.
<script setup lang="ts">import { Button, ButtonGroup } from "opui-css/vue"</script>
<template> <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></template><!--[--><div class="ui-button-group ui-small ui-outlined" role="group"> <!--[--><button class="ui-button"><!--[-->Small<!--]--></button ><button class="ui-button"><!--[-->Small<!--]--></button ><button class="ui-button"><!--[-->Small<!--]--></button ><!--]--></div><div class="ui-button-group ui-outlined" role="group"> <!--[--><button class="ui-button"><!--[-->Default<!--]--></button ><button class="ui-button"><!--[-->Default<!--]--></button ><button class="ui-button"><!--[-->Default<!--]--></button ><!--]--></div><div class="ui-button-group ui-large ui-outlined" role="group"> <!--[--><button class="ui-button"><!--[-->Large<!--]--></button ><button class="ui-button"><!--[-->Large<!--]--></button ><button class="ui-button"><!--[-->Large<!--]--></button ><!--]--></div><!--]-->Vertical orientation
Change the layout of the group with the orientation="vertical" prop.
<script setup lang="ts">import { Button, ButtonGroup } from "opui-css/vue"
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>`</script>
<template> <div class="example-row"> <ButtonGroup orientation="vertical"> <Button aria-label="Up"> <span v-html="plusIcon"></span> </Button> <Button aria-label="Decrease"> <span v-html="minusIcon"></span> </Button> </ButtonGroup>
<ButtonGroup orientation="vertical" variant="outlined"> <Button aria-label="Up"> <span v-html="plusIcon"></span> </Button> <Button aria-label="Decrease"> <span v-html="minusIcon"></span> </Button> </ButtonGroup>
<ButtonGroup orientation="vertical" variant="tonal"> <Button aria-label="Up"> <span v-html="plusIcon"></span> </Button> <Button aria-label="Decrease"> <span v-html="minusIcon"></span> </Button> </ButtonGroup>
<ButtonGroup orientation="vertical" variant="filled"> <Button aria-label="Up"> <span v-html="plusIcon"></span> </Button> <Button aria-label="Decrease"> <span v-html="minusIcon"></span> </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></template><!--[--><div class="example-row"> <div class="ui-button-group ui-vertical" role="group"> <!--[--><button class="ui-button" aria-label="Up"> <!--[--><span ><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></span ><!--]--></button ><button class="ui-button" aria-label="Decrease"> <!--[--><span ><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></span ><!--]--></button ><!--]--> </div> <div class="ui-button-group ui-outlined ui-vertical" role="group"> <!--[--><button class="ui-button" aria-label="Up"> <!--[--><span ><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></span ><!--]--></button ><button class="ui-button" aria-label="Decrease"> <!--[--><span ><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></span ><!--]--></button ><!--]--> </div> <div class="ui-button-group ui-tonal ui-vertical" role="group"> <!--[--><button class="ui-button" aria-label="Up"> <!--[--><span ><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></span ><!--]--></button ><button class="ui-button" aria-label="Decrease"> <!--[--><span ><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></span ><!--]--></button ><!--]--> </div> <div class="ui-button-group ui-filled ui-vertical" role="group"> <!--[--><button class="ui-button" aria-label="Up"> <!--[--><span ><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></span ><!--]--></button ><button class="ui-button" aria-label="Decrease"> <!--[--><span ><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></span ><!--]--></button ><!--]--> </div></div><div class="example-row"> <div class="ui-button-group ui-vertical" role="group"> <!--[--><button class="ui-button"><!--[-->Up<!--]--></button ><button class="ui-button"><!--[-->Down<!--]--></button ><!--]--> </div> <div class="ui-button-group ui-outlined ui-vertical" role="group"> <!--[--><button class="ui-button"><!--[-->Up<!--]--></button ><button class="ui-button"><!--[-->Down<!--]--></button ><!--]--> </div> <div class="ui-button-group ui-tonal ui-vertical" role="group"> <!--[--><button class="ui-button"><!--[-->Up<!--]--></button ><button class="ui-button"><!--[-->Down<!--]--></button ><!--]--> </div> <div class="ui-button-group ui-filled ui-vertical" role="group"> <!--[--><button class="ui-button"><!--[-->Up<!--]--></button ><button class="ui-button"><!--[-->Down<!--]--></button ><!--]--> </div></div><!--]-->Disabled
Disable individual buttons within a group by setting the disabled
prop on each Button.
<script setup lang="ts">import { Button, ButtonGroup } from "opui-css/vue"</script>
<template> <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></template><!--[--><div class="ui-button-group ui-filled" role="group"> <!--[--><button class="ui-button"><!--[-->Enabled<!--]--></button ><button class="ui-button ui-disabled" disabled> <!--[-->Disabled<!--]--></button ><button class="ui-button"><!--[-->Enabled<!--]--></button ><!--]--></div><div class="ui-button-group ui-primary ui-filled" role="group"> <!--[--><button class="ui-button"><!--[-->Enabled<!--]--></button ><button class="ui-button ui-disabled" disabled> <!--[-->Disabled<!--]--></button ><button class="ui-button"><!--[-->Enabled<!--]--></button ><!--]--></div><!--]-->Anatomy
-
Container:
<element role="group" class="ui-button-group"> - Buttons: Button
API
Button group
Button
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"].ui-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) );
&.ui-outlined { --_divider-color: var(--_border-color); }
&.ui-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(.ui-button-group.ui-critical, .ui-button-group.ui-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(.ui-button-group.ui-critical) & { --_color: var(--critical); }
:where(.ui-button-group.ui-primary) & { --_color: var(--primary); }
:where(.ui-button-group.ui-small) & { --_min-height: 1.875rem; padding-block: 0; padding-inline: 1ex; }
:where(.ui-button-group.ui-large) & { --_min-height: 2.875rem; padding-inline: 4ex; }
:where(.ui-button-group.ui-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(.ui-button-group.ui-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(.ui-button-group.ui-filled) & { --_bg-color: var(--_accent); --_text-color: var(--_accent-contrast); --_divider-color: currentColor;
:where(.ui-button-group.ui-critical, .ui-button-group.ui-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(.ui-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 ); } } } }
&.ui-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 ); } } } } }}