Skip to main content

Button Group

Button Groups display a set of related actions as a row of connected or pill-shaped buttons. They support standalone actions, single selection, or multiple selection — replacing the deprecated MD3 Segmented Button. Follows the Material Design 3 Button Group Expressive specification.

Usage​

import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
import { useState } from 'react'

export default function App() {
const [value, setValue] = useState('left')

return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }}>
<ButtonGroup
selectionMode="single"
value={value}
onValueChange={setValue}
items={[
{ value: 'left', label: 'Left', leadingIcon: 'format-align-left' },
{ value: 'center', label: 'Center', leadingIcon: 'format-align-center' },
{ value: 'right', label: 'Right', leadingIcon: 'format-align-right' },
]}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Variants​

VariantUse case
standardPill-shaped buttons separated by a small gap
connectedButtons read as a single segmented bar, divided by a 2dp separator gap; the selected button morphs to a smaller corner radius
import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
import { useState } from 'react'

const items = [
{ value: 'left', label: 'Left', leadingIcon: 'format-align-left' },
{ value: 'center', label: 'Center', leadingIcon: 'format-align-center' },
{ value: 'right', label: 'Right', leadingIcon: 'format-align-right' },
]

export default function App() {
const [standard, setStandard] = useState('left')
const [connected, setConnected] = useState('center')

return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, justifyContent: 'center', padding: 16, gap: 16 }}>
<ButtonGroup
variant="standard"
selectionMode="single"
value={standard}
onValueChange={setStandard}
items={items}
/>
<ButtonGroup
variant="connected"
selectionMode="single"
value={connected}
onValueChange={setConnected}
items={items}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Selection Modes​

selectionMode controls how the group tracks state.

ModeBehaviorvalue type
none (default)Items act as standalone actions; no selection state. Use onItemPress—
singleAt most one item can be selected. Pressing the selected item deselects it, and onValueChange fires with nullstring | null
multipleAny number of items can be selectedstring[]

Both single and multiple modes can be controlled (value + onValueChange) or uncontrolled (defaultValue).

Single select​

import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
import { useState } from 'react'

export default function App() {
const [alignment, setAlignment] = useState('left')

return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }}>
<ButtonGroup
variant="connected"
selectionMode="single"
value={alignment}
onValueChange={setAlignment}
items={[
{ value: 'left', label: 'Left', leadingIcon: 'format-align-left' },
{ value: 'center', label: 'Center', leadingIcon: 'format-align-center' },
{ value: 'right', label: 'Right', leadingIcon: 'format-align-right' },
]}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Multiple select​

import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'
import { useState } from 'react'

export default function App() {
const [formatting, setFormatting] = useState(['bold'])

return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }}>
<ButtonGroup
variant="standard"
selectionMode="multiple"
value={formatting}
onValueChange={setFormatting}
items={[
{ value: 'bold', label: 'Bold', leadingIcon: 'format-bold' },
{ value: 'italic', label: 'Italic', leadingIcon: 'format-italic' },
{ value: 'underline', label: 'Underline', leadingIcon: 'format-underline' },
]}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Action group (no selection)​

import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }}>
<ButtonGroup
variant="standard"
selectionMode="none"
onItemPress={(value) => console.log('pressed', value)}
items={[
{ value: 'reply', label: 'Reply', leadingIcon: 'reply' },
{ value: 'forward', label: 'Forward', leadingIcon: 'share' },
{ value: 'archive', label: 'Archive', leadingIcon: 'archive-outline' },
]}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Sizes​

Five MD3 Expressive sizes are supported. Heights and corner radii scale together.

SizeUse case
extraSmallDense toolbars
small (default)Standard inline controls
mediumComfortable density
largeProminent toolbars
extraLargeHero-level emphasis
import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

const sizes = ['extraSmall', 'small', 'medium', 'large', 'extraLarge']

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, justifyContent: 'center', padding: 16, gap: 12 }}>
{sizes.map((size) => (
<ButtonGroup
key={size}
variant="connected"
selectionMode="single"
size={size}
defaultValue="center"
items={[
{ value: 'left', leadingIcon: 'format-align-left', accessibilityLabel: 'Align left' },
{ value: 'center', leadingIcon: 'format-align-center', accessibilityLabel: 'Align center' },
{ value: 'right', leadingIcon: 'format-align-right', accessibilityLabel: 'Align right' },
]}
/>
))}
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Icons​

Each item's leadingIcon and trailingIcon accept the same forms as Button — a string name (resolved via the theme's iconResolver, for example mdiResolver for MaterialDesignIcons), a pre-rendered React element, or a render function that receives { size, color }. See the Icons guide for full details.

Items can omit label entirely when an icon plus accessibilityLabel is enough.

Disabled​

Disable the entire group with disabled, or disable individual items with item.disabled.

import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, justifyContent: 'center', padding: 16, gap: 16 }}>
<ButtonGroup
variant="connected"
selectionMode="single"
defaultValue="b"
disabled
items={[
{ value: 'a', label: 'One' },
{ value: 'b', label: 'Two' },
{ value: 'c', label: 'Three' },
]}
/>
<ButtonGroup
variant="standard"
selectionMode="single"
defaultValue="a"
items={[
{ value: 'a', label: 'Available' },
{ value: 'b', label: 'Locked', disabled: true },
{ value: 'c', label: 'Available' },
]}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Custom colors​

Override unselected and selected container/content colors independently. State-layer colors (hover, press) are auto-derived.

import { ButtonGroup } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }}>
<ButtonGroup
variant="connected"
selectionMode="single"
defaultValue="b"
containerColor="#E8DEF8"
contentColor="#4A148C"
selectedContainerColor="#6750A4"
selectedContentColor="#FFFFFF"
items={[
{ value: 'a', label: 'Day', leadingIcon: 'weather-sunny' },
{ value: 'b', label: 'Night', leadingIcon: 'weather-night' },
{ value: 'c', label: 'Auto', leadingIcon: 'theme-light-dark' },
]}
/>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Props​

PropTypeDefaultRequiredDescription
selectionModeenum-No-
valuestring | string[]-NoCurrently selected item value (controlled). Currently selected item values (controlled).
defaultValuestring | string[]-NoInitial selected item value (uncontrolled). Initial selected item values (uncontrolled).
onValueChange((value: string) => void) | ((value: string[]) => void)-NoCalled when the selection changes.
onItemPress((value: string) => void) | ((value: string) => void) | ((value: string) => void)-NoCalled when an item is pressed. Only available when `selectionMode` is `'none'`.
itemsButtonGroupItem[]-YesThe buttons to render in the group.
variantenum'standard'NoVisual variant.
sizeenum'small'NoButton size.
iconSizenumber-NoOverride the icon size (in dp) for all items. Defaults to a size that matches the group `size`.
disabledboolean-NoDisable the entire group. Per-item `disabled` is also honored.
containerColorstring-NoOverride the container (background) color of unselected items. State-layer colors (hover, press) are derived automatically.
contentColorstring-NoOverride the content (label and icon) color of unselected items.
selectedContainerColorstring-NoOverride the container color of selected items.
selectedContentColorstring-NoOverride the content color of selected items.
labelStyleStyleProp<TextStyle>-NoStyle applied to each item label.
styleStyleProp<ViewStyle>-NoStyle applied to the root container.
accessibilityLabelstring-NoAccessibility label for the group container.
testIDstring-NoTest ID forwarded to the root container.
hrefAttrs{ target?: string; rel?: string; download?: string | boolean; }-NoWeb only. The `target`, `rel` and `download` of the `<a>` that a view with an `href` renders, for example under Expo Router's `<Link asChild>`. `target: '_blank'` opens the link in a new tab. Native ignores it.
onKeyDown(event: { nativeEvent: { key?: string; }; }) => void-No-