UnflowUI
Components

ButtonGroup

A connected row of Button elements sharing one container, size, and shape — usable as an "action" toolbar or a "tab" style segmented switch.

Overview

ButtonGroup renders a set of Buttons inside a single bordered container, applying one shared size and rounded shape to every button in the row. Pass the buttons declaratively via the buttons array — each entry accepts the same props as Button (minus size, which the group controls).

Two behaviour modes are supported:

  • action (default) — an independent group of buttons, each firing its own onClick. Think toolbar or split-action controls.
  • tab — a segmented switch. The button at activeIndex gets a data-active="true" attribute (styled with a highlighted background); the rest render normally.

Import

import { ButtonGroup } from '@unflow.io/ui/components/ButtonGroup';

Props

PropTypeDefaultDescription
buttonsOmit<IButton, 'size'>[]Required. Buttons to render, in order. Each entry accepts any Button prop except size
behaviour'action' | 'tab''action'Whether buttons act independently or as a segmented switch
activeIndexnumber-1Index of the active button when behaviour="tab". Sets data-active="true" on that button
size'xs' | 'sm' | 'md' | 'lg''sm'Size applied to the container and every button in the group
roundedbooleanRenders the container and buttons as fully rounded (pill shape) instead of the default corner radius

All standard HTMLDivElement props are forwarded to the container. Passing children overrides the buttons-driven rendering entirely.

Action group

<ButtonGroup
  buttons={[
    { label: 'Cut', onClick: () => alert('Cut') },
    { label: 'Copy', onClick: () => alert('Copy') },
    { label: 'Paste', onClick: () => alert('Paste') },
  ]}
/>

Tab group

Set behaviour="tab" and drive activeIndex from state — each button's own onClick is responsible for updating it.

const [activeIndex, setActiveIndex] = useState(0);

<ButtonGroup
  behaviour="tab"
  activeIndex={activeIndex}
  buttons={[
    { label: 'Day', onClick: () => setActiveIndex(0) },
    { label: 'Week', onClick: () => setActiveIndex(1) },
    { label: 'Month', onClick: () => setActiveIndex(2) },
  ]}
/>

Sizes

<ButtonGroup size="xs" buttons={[{ label: 'XS 1' }, { label: 'XS 2' }]} />
<ButtonGroup size="sm" buttons={[{ label: 'SM 1' }, { label: 'SM 2' }]} />
<ButtonGroup size="md" buttons={[{ label: 'MD 1' }, { label: 'MD 2' }]} />
<ButtonGroup size="lg" buttons={[{ label: 'LG 1' }, { label: 'LG 2' }]} />

Custom styling

Buttons keep their own variant, icons, and className — mix them freely, and combine with rounded for a pill-shaped group.

<ButtonGroup
  rounded
  buttons={[
    { startIcon: <ArrowLeft />, label: 'Previous', variant: 'secondary' },
    { startIcon: <Trash />, variant: 'destructiveSecondary' },
    { label: 'Next', variant: 'primary', endIcon: <ArrowRight /> },
  ]}
/>

Accessibility

  • Each button renders as a native <button> via Button, so click and keyboard activation work out of the box.
  • behaviour="tab" only applies a visual data-active state — it does not add role="tablist"/role="tab" or aria-selected. If you're building an actual tabbed interface, add that ARIA wiring yourself (or reach for a dedicated tabs pattern).
  • Disabled buttons in the buttons array behave like any disabled Buttondisabled and aria-disabled are set, and they're skipped by pointer/keyboard interaction.