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 '@onlynative/components'
import { ThemeProvider } from '@onlynative/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>
<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 share edges as a single segmented bar; the selected button morphs to a smaller corner radius
import { ButtonGroup } from '@onlynative/components'
import { ThemeProvider } from '@onlynative/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>
<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
singleExactly one item can be selectedstring | 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 '@onlynative/components'
import { ThemeProvider } from '@onlynative/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>
<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 '@onlynative/components'
import { ThemeProvider } from '@onlynative/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>
<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 '@onlynative/components'
import { ThemeProvider } from '@onlynative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<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 '@onlynative/components'
import { ThemeProvider } from '@onlynative/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>
<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 MaterialCommunityIcons by default), 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 '@onlynative/components'
import { ThemeProvider } from '@onlynative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<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 '@onlynative/components'
import { ThemeProvider } from '@onlynative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<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.