Merge pull request #32232 from Believe-SA/blv/button-toggle

feat(bui): Introduce ToggleButton/ToggleButtonGroup
This commit is contained in:
Charles de Dreuille
2026-01-13 12:02:12 +00:00
committed by GitHub
22 changed files with 1707 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
toggleButtonGroupPropDefs,
toggleButtonGroupUsageSnippet,
toggleButtonGroupSingleSnippet,
toggleButtonGroupMultipleSnippet,
toggleButtonGroupVerticalSnippet,
toggleButtonGroupDisabledSnippet,
toggleButtonGroupDisallowEmptySnippet,
toggleButtonGroupIconsSnippet,
toggleButtonGroupIconsOnlySnippet,
toggleButtonGroupSurfacesSnippet,
} from './toggle-button-group.props';
import { ToggleButtonGroupSnippet } from '@/snippets/stories-snippets';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ToggleButtonGroupDefinition } from '../utils/definitions';
<PageTitle
title="ToggleButtonGroup"
description="A grouping container for related [ToggleButtons](./toggle-button) supporting single or multiple selection."
/>
<Snippet
align="center"
py={4}
preview={<ToggleButtonGroupSnippet story="SingleSelection" />}
code={toggleButtonGroupSingleSnippet}
/>
## Usage
<CodeBlock code={toggleButtonGroupUsageSnippet} />
## API reference
<PropsTable data={toggleButtonGroupPropDefs} />
## Examples
### Surfaces
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="Surfaces" />}
code={toggleButtonGroupSurfacesSnippet}
/>
### Single Selection
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="SingleSelection" />}
code={toggleButtonGroupSingleSnippet}
/>
### Multiple Selection
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="MultipleSelection" />}
code={toggleButtonGroupMultipleSnippet}
/>
### Icons and Text
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="WithIcons" />}
code={toggleButtonGroupIconsSnippet}
/>
### Icons Only
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="IconsOnly" />}
code={toggleButtonGroupIconsOnlySnippet}
/>
### Disallow Empty Selection
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="DisallowEmptySelection" />}
code={toggleButtonGroupDisallowEmptySnippet}
/>
### Vertical Orientation
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="Orientation" />}
code={toggleButtonGroupVerticalSnippet}
/>
### Disabled
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonGroupSnippet story="DisabledGroup" />}
code={toggleButtonGroupDisabledSnippet}
/>
<Theming definition={ToggleButtonGroupDefinition} />
<ChangelogComponent component="toggle-button-group" />
@@ -0,0 +1,142 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const toggleButtonGroupPropDefs: Record<string, PropDef> = {
selectionMode: {
type: 'enum',
values: ['single', 'multiple'],
default: 'single',
},
orientation: {
type: 'enum',
values: ['horizontal', 'vertical'],
default: 'horizontal',
responsive: true,
},
selectedKeys: { type: 'enum', values: ['Iterable<Key>'] },
defaultSelectedKeys: { type: 'enum', values: ['Iterable<Key>'] },
onSelectionChange: { type: 'enum', values: ['(keys) => void'] },
isDisabled: { type: 'boolean', default: 'false' },
disallowEmptySelection: { type: 'boolean', default: 'false' },
...classNamePropDefs,
...stylePropDefs,
};
export const toggleButtonGroupUsageSnippet = `import { ToggleButtonGroup, ToggleButton } from '@backstage/ui';
<ToggleButtonGroup selectionMode="single">
<ToggleButton id="dogs">Dogs</ToggleButton>
<ToggleButton id="cats">Cats</ToggleButton>
<ToggleButton id="birds">Birds</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupSingleSnippet = `<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['dogs']}>
<ToggleButton id="dogs">Dogs</ToggleButton>
<ToggleButton id="cat">Cats</ToggleButton>
<ToggleButton id="bird">Birds</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupMultipleSnippet = `<ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['frontend']}>
<ToggleButton id="frontend">Frontend</ToggleButton>
<ToggleButton id="backend">Backend</ToggleButton>
<ToggleButton id="platform">Platform</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupVerticalSnippet = `<ToggleButtonGroup selectionMode="single" orientation="vertical">
<ToggleButton id="morning">Morning</ToggleButton>
<ToggleButton id="afternoon">Afternoon</ToggleButton>
<ToggleButton id="evening">Evening</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupDisabledSnippet = `<ToggleButtonGroup selectionMode="single" isDisabled>
<ToggleButton id="cat">Cat</ToggleButton>
<ToggleButton id="dog">Dog</ToggleButton>
<ToggleButton id="bird">Bird</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupDisallowEmptySnippet = `<ToggleButtonGroup selectionMode="single" disallowEmptySelection defaultSelectedKeys={['one']}>
<ToggleButton id="one">One</ToggleButton>
<ToggleButton id="two">Two</ToggleButton>
<ToggleButton id="three">Three</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupIconsSnippet = `import { RiCloudLine, RiStarFill, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
<ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['cloud']}>
<ToggleButton id="cloud" aria-label="Cloud" iconStart={<RiCloudLine />} />
<ToggleButton
id="starred"
aria-label="Starred"
iconStart={<RiStarFill />}
/>
<ToggleButton id="star" iconStart={<RiStarLine />}>
Star
</ToggleButton>
<ToggleButton id="next" iconEnd={<RiArrowRightSLine />}>
Next
</ToggleButton>
</ToggleButtonGroup>`;
export const toggleButtonGroupIconsOnlySnippet = `import { RiCloudLine, RiStarLine, RiArrowRightSLine } from '@remixicon/react';
<ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['cloud']}>
<ToggleButton id="cloud" iconStart={<RiCloudLine />} />
<ToggleButton id="star" iconStart={<RiStarLine />} />
<ToggleButton id="next" iconEnd={<RiArrowRightSLine />} />
</ToggleButtonGroup>`;
export const toggleButtonGroupSurfacesSnippet = `<Flex direction="column" gap="4">
<Flex direction="column" gap="4">
<Text>Default</Text>
<Flex align="center" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex align="center" surface="0" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex align="center" surface="1" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex align="center" surface="2" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex align="center" surface="3" p="4" gap="4">
<ToggleButtonGroup selectionMode="single" defaultSelectedKeys={['option1']}>
<ToggleButton id="option1">Option 1</ToggleButton>
<ToggleButton id="option2">Option 2</ToggleButton>
<ToggleButton id="option3">Option 3</ToggleButton>
</ToggleButtonGroup>
</Flex>
</Flex>
</Flex>`;
+106
View File
@@ -0,0 +1,106 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { ToggleButtonSnippet } from '@/snippets/stories-snippets';
import {
toggleButtonPropDefs,
toggleButtonUsageSnippet,
toggleButtonSurfacesSnippet,
toggleButtonSizesSnippet,
toggleButtonIconsSnippet,
toggleButtonDisabledSnippet,
toggleButtonControlledSnippet,
toggleButtonFunctionChildrenSnippet,
} from './toggle-button.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { ToggleButtonDefinition } from '../utils/definitions';
<PageTitle
title="ToggleButton"
description="A button that toggles between selected and unselected states. Can be used as a single toggle or as part of a [ToggleButtonGroup](./toggle-button-group)."
/>
<Snippet
align="center"
py={4}
preview={<ToggleButtonSnippet story="Default" />}
code={toggleButtonUsageSnippet}
/>
## Usage
<CodeBlock code={toggleButtonUsageSnippet} />
## API reference
<PropsTable data={toggleButtonPropDefs} />
## Examples
### Surfaces
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonSnippet story="Surfaces" />}
code={toggleButtonSurfacesSnippet}
/>
### Sizes
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonSnippet story="Sizes" />}
code={toggleButtonSizesSnippet}
/>
### With Icons
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonSnippet story="WithIcons" />}
code={toggleButtonIconsSnippet}
/>
### Disabled
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonSnippet story="Disabled" />}
code={toggleButtonDisabledSnippet}
/>
### Controlled
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonSnippet story="Controlled" />}
code={toggleButtonControlledSnippet}
/>
### Dynamic Content with Function Children
The `children` prop can be a function that receives render props, allowing you to dynamically customize the button content based on component state (such as `isSelected`, `isDisabled`, `isHovered`, etc.).
<Snippet
align="center"
py={4}
open
preview={<ToggleButtonSnippet story="DynamicContent" />}
code={toggleButtonFunctionChildrenSnippet}
/>
<Theming definition={ToggleButtonDefinition} />
<ChangelogComponent component="toggle-button" />
+195
View File
@@ -0,0 +1,195 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const toggleButtonPropDefs: Record<string, PropDef> = {
size: {
type: 'enum',
values: ['small', 'medium'],
default: 'small',
responsive: true,
},
onSurface: {
type: 'enum',
values: ['0', '1', '2', '3', 'danger', 'warning', 'success', 'auto'],
description: 'Surface level this toggle is placed on',
responsive: true,
},
iconStart: { type: 'enum', values: ['ReactNode'] },
iconEnd: { type: 'enum', values: ['ReactNode'] },
isSelected: { type: 'boolean' },
defaultSelected: { type: 'boolean' },
onChange: { type: 'enum', values: ['(isSelected: boolean) => void'] },
isDisabled: { type: 'boolean', default: 'false' },
children: {
type: 'enum',
values: ['ReactNode', '(values: ToggleButtonRenderProps) => ReactNode'],
description:
'The children of the component. A function may be provided to alter the children based on component state (such as `isSelected`, `isDisabled`, `isHovered`, etc.).',
},
...classNamePropDefs,
...stylePropDefs,
};
export const toggleButtonUsageSnippet = `import { ToggleButton } from '@backstage/ui';
<ToggleButton>Toggle</ToggleButton>`;
export const toggleButtonSurfacesSnippet = `<Flex direction="column" gap="4">
<Flex direction="column" gap="4">
<Text>Default</Text>
<Flex align="center" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex align="center" surface="0" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex align="center" surface="1" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex align="center" surface="2" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex align="center" surface="3" p="4">
<ToggleButton>Toggle</ToggleButton>
</Flex>
</Flex>
</Flex>`;
export const toggleButtonSizesSnippet = `<Flex align="center">
<ToggleButton size="small">Small</ToggleButton>
<ToggleButton size="medium">Medium</ToggleButton>
</Flex>`;
export const toggleButtonIconsSnippet = `import { RiStarLine, RiStarFill, RiCheckLine } from '@remixicon/react';
<Flex align="center">
<ToggleButton iconStart={<RiStarLine />}>Favorite</ToggleButton>
<ToggleButton iconStart={<RiStarFill />} defaultSelected>Starred</ToggleButton>
<ToggleButton iconEnd={<RiCheckLine />}>Confirm</ToggleButton>
</Flex>`;
export const toggleButtonDisabledSnippet = `<Flex align="center">
<ToggleButton isDisabled>Disabled</ToggleButton>
<ToggleButton defaultSelected isDisabled>Selected</ToggleButton>
</Flex>`;
export const toggleButtonControlledSnippet = `import { useState } from 'react';
import { RiStarFill, RiStarLine } from '@remixicon/react';
const [selected, setSelected] = useState(false);
<ToggleButton
isSelected={selected}
onChange={setSelected}
iconStart={selected ? <RiStarFill /> : <RiStarLine />}
>
{selected ? 'Starred' : 'Not starred'}
</ToggleButton>`;
export const toggleButtonFunctionChildrenSnippet = `import { RiStarFill, RiStarLine } from '@remixicon/react';
<Flex direction="column" gap="4">
<Flex direction="column" gap="2">
<Text weight="bold">Example 1: Selection State</Text>
<Flex align="center" gap="2">
<ToggleButton defaultSelected>
{({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')}
</ToggleButton>
<ToggleButton>
{({ isSelected }) => (isSelected ? '✓ Selected' : 'Not Selected')}
</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="2">
<Text weight="bold">Example 2: Multiple States</Text>
<Flex align="center" gap="2">
<ToggleButton defaultSelected>
{({ isSelected, isHovered }) => {
const states = [];
if (isSelected) states.push('on');
else states.push('off');
if (isHovered) states.push('hovered');
return \`Email (\${states.join(', ')})\`;
}}
</ToggleButton>
<ToggleButton>
{({ isSelected, isHovered }) => {
const states = [];
if (isSelected) states.push('on');
else states.push('off');
if (isHovered) states.push('hovered');
return \`Push (\${states.join(', ')})\`;
}}
</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="2">
<Text weight="bold">Example 3: Conditional Icons</Text>
<Flex align="center" gap="2">
<ToggleButton>
{({ isSelected }) => (
<>
{isSelected ? <RiStarFill /> : <RiStarLine />}
<span>{isSelected ? 'Starred' : 'Star'}</span>
</>
)}
</ToggleButton>
</Flex>
</Flex>
<Flex direction="column" gap="2">
<Text weight="bold">Example 4: Status Indicators</Text>
<Flex align="center" gap="2">
<ToggleButton defaultSelected>
{({ isSelected }) => (
<Flex align="center" gap="2">
<span
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: isSelected
? 'var(--bui-fg-success)'
: 'var(--bui-fg-secondary)',
}}
/>
<span>Active</span>
</Flex>
)}
</ToggleButton>
<ToggleButton>
{({ isSelected }) => (
<Flex align="center" gap="2">
<span
style={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: isSelected
? 'var(--bui-fg-danger)'
: 'var(--bui-fg-secondary)',
}}
/>
<span>Inactive</span>
</Flex>
)}
</ToggleButton>
</Flex>
</Flex>
</Flex>`;
@@ -30,6 +30,8 @@ import * as TableStories from '../../../packages/ui/src/components/Table/stories
import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories';
import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories';
import * as VisuallyHiddenStories from '../../../packages/ui/src/components/VisuallyHidden/VisuallyHidden.stories';
import * as ToggleButtonStories from '../../../packages/ui/src/components/ToggleButton/ToggleButton.stories';
import * as ToggleButtonGroupStories from '../../../packages/ui/src/components/ToggleButtonGroup/ToggleButtonGroup.stories';
// Helper function to create snippet components
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -75,6 +77,10 @@ export const HeaderSnippet = createSnippetComponent(HeaderStories);
export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
export const TableSnippet = createSnippetComponent(TableStories);
export const TagGroupSnippet = createSnippetComponent(TagGroupStories);
export const ToggleButtonSnippet = createSnippetComponent(ToggleButtonStories);
export const ToggleButtonGroupSnippet = createSnippetComponent(
ToggleButtonGroupStories,
);
export const VisuallyHiddenSnippet = createSnippetComponent(
VisuallyHiddenStories,
);
+8
View File
@@ -116,6 +116,14 @@ export const components: Page[] = [
title: 'TextField',
slug: 'text-field',
},
{
title: 'ToggleButton',
slug: 'toggle-button',
},
{
title: 'ToggleButtonGroup',
slug: 'toggle-button-group',
},
{
title: 'Tooltip',
slug: 'tooltip',