GrButtonGroup

  • core
  • actions
  • API · 7 props

Groups related buttons into a compact action cluster.

import { GrButtonGroup } from '@feugene/granularity/components/GrButtonGroup'

Overview

When to take it

  • actions of the same class stand side by side — a toolbar, a “Save / Cancel” row, view switches;
  • the row has to read as one whole — the inner borders are not doubled, the rounding stays at the edges;
  • the styling is set once — size, tone and variant are handed to every button at once;
  • the group needs a name — ariaLabel explains what unites these buttons.

When to take something else

NeedTake
A value is selected rather than an action performedGrSegmented
There are many actions and they are hiddenGrDropdownMenu
There is a single actionGrButton
Sections with different contentGrTabs

A button group and a segmented control look alike and mean different things: here every button does something and nothing stays selected after the press, there one of the options is always active.

Variants every tone and view, rendered by the real component

Variants · GrButtonGroup
tone \ variantprimarysecondaryoutlineghostghost-border
primary
neutral
success
warning
danger
info
slate
azure

Sizes

  • xs
  • sm
  • md
  • lg

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • Segmented switcher of one context

    The group joins adjacent actions into one control: the edges are glued, the pressed segment is the state of the screen, and aria-label tells what exactly is being switched.

  • Size, variant and tone set once

    size, variant and tone are set on the group and reach every button; a button’s own prop still wins — that is how the dangerous action stays red in a neutral row.

  • A column and a row with a gap

    orientation="vertical" stacks the buttons and moves the rounded corners to the top and bottom edges; :attached="false" gives a plain row where every button keeps its own radii and borders.

API generated from the package · cannot drift

Props

PropTypedefaultDescription
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedThe tone of all the buttons in the group. The prop of a button itself is stronger.
variantGrButtonVariant | undefinedundefinedThe weight of all the buttons in the group: `primary`, `outline`, `ghost` and so on. The prop of a button itself is stronger.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedStyling shared by the buttons of the group. The prop of a button itself is stronger.
ariaLabelstring | undefinedundefinedThe accessible name of the group: without it the buttons read as unrelated.
orientationGrButtonGroupOrientation | undefined"horizontal"The direction of the row: `vertical` stacks the buttons in a column. `horizontal` by default.
attachedboolean | undefinedtrueGlue the buttons into one block. `false` is an ordinary row with a gap: every button keeps its own radii and borders. Has no effect in the tray.
appearanceGrButtonGroupAppearance | undefinedundefinedThe look of the group. `default` is a row according to `attached`; `tray` is a tray: the segments lie on a shared backing with a gap, transparent, and the selected one (`aria-pressed="true"` or `aria-current`) is a sheet with a border over the tray. In the tray the group decides the weight and tone of a segment; the button's own prop is stronger. Defaults to `default`; a theme changes it through `componentDefaults` (Clay).

Slots

SlotTypeDescription
defaultanyThe buttons of the group.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrButtonGroupAppearance, GrButtonGroupContext, GrButtonGroupConfigurableProps } from '@feugene/granularity/components/GrButtonGroup'

Filedist/types/components/GrButtonGroup/config.d.ts

grButtonGroupConfigconst
const grButtonGroupConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrButtonGroup">;

Filedist/types/components/shared/buttonGroupContext.d.ts

GrButtonGroupAppearancetype
type GrButtonGroupAppearance = 'default' | 'tray';
GrButtonGroupContexttype
type GrButtonGroupContext = {
    size: ComputedRef<GrButtonSize | undefined>;
    variant: ComputedRef<GrButtonVariant | undefined>;
    tone: ComputedRef<GrButtonTone | undefined>;
    appearance?: ComputedRef<GrButtonGroupAppearance>;
};
useGrButtonGroupfunction
function useGrButtonGroup(): GrButtonGroupContext | undefined;

Filedist/types/components/GrButtonGroup/defaults.d.ts

GrButtonGroupConfigurablePropsinterface
interface GrButtonGroupConfigurableProps {
    appearance: GrButtonGroupAppearance;
}

Filedist/types/components/GrButtonGroup/GrButtonGroup.vue.d.ts

GrButtonGroupOrientationtype
type GrButtonGroupOrientation = 'horizontal' | 'vertical';
GrButtonGroupPropsinterface
interface GrButtonGroupProps {
    ariaLabel?: string;
    orientation?: GrButtonGroupOrientation;
    attached?: boolean;
    appearance?: GrButtonGroupAppearance;
    size?: GrButtonSize;
    variant?: GrButtonVariant;
    tone?: GrButtonTone;
}

Usage from the library prose

Styling in one line
vue
<GrButtonGroup aria-label="Period" size="sm" variant="outline" tone="neutral">
  <GrButton>Day</GrButton>
  <GrButton>Week</GrButton>
  <GrButton tone="primary">Month</GrButton>
</GrButtonGroup>

The size, variant and tone of the group reach the buttons through context. The resolution order: the button’s prop → the group → GrConfigProvider → the default — the group is closer to the button than the global provider, so it beats the provider, but not the button’s own prop.

The context is available from the outside as well — useGrButtonGroup(), if a control of your own is built on top of the group.

Wrappers do not break the row

The gluing counts the links of the group — direct children that either are a button or contain one. A button can therefore be wrapped into a tooltip, a v-if wrapper or a router link, and the row stays whole:

vue
<GrButtonGroup aria-label="Document">
  <GrButton>Open</GrButton>
  <GrTooltip content="The copy will appear next to it">
    <GrButton>Duplicate</GrButton>
  </GrTooltip>
</GrButtonGroup>

A non-button child (a separator, a label) does not enter the gluing and does not take any rounding for itself.

Orientation and the mode without gluing
PropWhat it does
orientation="vertical"a column: the rounding moves to the top and bottom edges of the row
:attached="false"an ordinary row with a gap, every button with its own radii
The tray

appearance="tray" — the segments are not glued but lie on a shared backing with padding and a 2px gap. An unselected segment is transparent, the selected one is a sheet with a border over the tray:

vue
<GrButtonGroup appearance="tray" size="sm" aria-label="Period">
  <GrButton :aria-pressed="range === 'day'" @click="range = 'day'">Day</GrButton>
  <GrButton :aria-pressed="range === 'week'" @click="range = 'week'">Week</GrButton>
</GrButtonGroup>
  • The selected one is found by aria-pressed="true" or aria-current. The button has no state of its own: selection is an attribute the consumer has to set for the screen reader anyway, and the tray reads the same one.
  • The tray decides the weight and the tone of a segment: transparent ghost, the selected one secondary, tone neutral. The button’s own prop is stronger, the group’s variant does not act in the tray, and neither does attached.
  • The backing of the tray is --gr-muted, the --gr-button-group-tray-bg hook.

The tray is styling, so it is set both by a theme (componentDefaults: that is how Clay turns it on) and by GrConfigProvider.

The radius

The outer corners of a glued group are rounded by the step radius of its segments — the same as on a standalone button (the theme’s --gr-button-radius-<step>); the group cancels the inner ones itself. The application handle --gr-button-radius sets one radius for buttons and groups at once, --gr-button-group-radius — only for the outer corners of groups:

css
.app { --gr-button-radius: 12px; }
The seam between solid segments

Neighbouring primary segments in a glued group are filled with one colour and merge: a “Publish ▾” split button would read as a single button with a chevron in its label. So the second of two neighbouring solid segments gets a light 1px seam — on the left in a horizontal group, on top in a vertical one.

The group only sets the --gr-button-divider variable, and the seam is drawn by the button’s own shadow: a box-shadow of its own from the group’s sheet would lose to the button’s utility at any specificity. A segment wrapped in a tooltip or a link gets no seam.

Accessibility

The root is declared role="group"; the name is set with the ariaLabel prop — without it a set of buttons reads as unrelated. The button under the cursor or with focus rises above its neighbours, so the focus ring is not clipped by the overlapping borders.