Skip to main content

Button

Buttons help people take actions and make choices with a single tap. Follows the Material Design 3 Button specification.

Usage​

import { Button } 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 }}>
<Button variant="filled" onPress={() => console.log('pressed')}>
Save
</Button>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Variants​

VariantUse case
filledPrimary action, highest emphasis
elevatedElevated surface, medium emphasis
outlinedSecondary action, medium emphasis
textLow emphasis, often in dialogs
tonalSecondary container fill
import { Button } 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: 12 }}>
<Button variant="filled" onPress={() => {}}>Filled</Button>
<Button variant="elevated" onPress={() => {}}>Elevated</Button>
<Button variant="outlined" onPress={() => {}}>Outlined</Button>
<Button variant="text" onPress={() => {}}>Text</Button>
<Button variant="tonal" onPress={() => {}}>Tonal</Button>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

With Icons​

leadingIcon and trailingIcon accept three forms — a string name (resolved via the theme's iconResolver, for example mdiResolver for MaterialDesignIcons), a pre-rendered React element from any icon library, or a render function that receives the resolved { size, color }. See the Icons guide for full details, including how to register a global iconResolver for Lucide, SF Symbols, or custom SVGs.

import { Button } 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: 12 }}>
<Button leadingIcon="plus" onPress={() => {}}>Add Item</Button>
<Button trailingIcon="arrow-right" onPress={() => {}}>Continue</Button>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Bring your own icon library​

Pass any element directly — useful for one-off icons or when you want full control over sizing and color:

import { Button } from '@rootnative/components'
import { Check, ArrowRight } from 'lucide-react-native'

<Button leadingIcon={<Check size={18} color="#fff" />}>Save</Button>
<Button trailingIcon={<ArrowRight size={18} color="#fff" />}>Continue</Button>

Or use a render function to receive the icon size and color the button resolved from its size, variant and current state, so the icon tracks them without hardcoding:

<Button leadingIcon={({ size, color }) => <Check size={size} color={color} />}>
Save
</Button>

iconSize and contentColor override what the function receives, if you need to depart from the resolved values:

<Button
leadingIcon={({ size, color }) => <Check size={size} color={color} />}
iconSize={20}
contentColor="#fff"
>
Save
</Button>

Disabled​

import { Button } 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 }}>
<Button disabled onPress={() => {}}>Disabled</Button>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

Loading​

Set loading while the action runs. The button shows an indeterminate CircularProgress in place of the leading icon, at the icon size and in the label color. It ignores presses, shows no hover or press state, and sets aria-busy. It keeps its colors and its focus, so a keyboard user stays on it. MD3 has no loading button, so this is a library addition.

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

export default function App() {
const [isSaving, setIsSaving] = useState(false)

const save = () => {
setIsSaving(true)
setTimeout(() => setIsSaving(false), 2000)
}

return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 16 }}>
<Button loading={isSaving} onPress={save} style={{ width: 160 }}>
Save
</Button>
</View>
</ThemeProvider>
</SafeAreaProvider>
)
}

With no leadingIcon, the spinner adds the icon and its gap, so the button gets wider while it loads. A width in style keeps the size, as above. Pass disabled as well if the button must also look disabled.

A button that opens a page must be a link on the web. A Button with an onPress that calls router.push or Linking.openURL renders a <button>. A crawler does not click a <button>, so it does not find the page.

Put the button inside Expo Router's <Link asChild>. Link gives the button role="link" and href. That role replaces the button role, so the button renders an <a href> on the web. A crawler follows it, and a visitor can copy it or open it with a modifier key. On native, Link handles the press: it navigates to a route, or opens an external URL with Linking.

import { Button } from '@rootnative/components'
import { Link } from 'expo-router'

<Link href="/docs" asChild>
<Button variant="text">Docs</Button>
</Link>

On the web, Link opens an external URL in the same tab. Do not use <Link target="_blank"> for a new tab: under asChild, Link gives target to the button as a plain prop, and react-native-web drops it. Give the button hrefAttrs. react-native-web writes it onto the <a>, and Expo Router then lets the browser open the new tab.

<Link href="https://github.com/rootnative" asChild>
<Button hrefAttrs={{ target: '_blank', rel: 'noopener' }}>GitHub</Button>
</Link>

hrefAttrs is web only, and native ignores it. @rootnative/components declares it on the React Native ViewProps type, so you do not need a type augmentation.

Props​

PropTypeDefaultRequiredDescription
childrenstring-YesText label rendered inside the button.
variantenumfilledNoVisual variant. Controls background, border, and text color.
sizeenumsmallNoMD3 Expressive size — one of `'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge'`. Sets container height, padding, icon size, and label typography.
shapeenumroundNoContainer shape. `'round'` is a full pill; `'square'` rests at a size-dependent rounded corner. Both morph squarer on press.
leadingIconIconSource-NoIcon rendered before the label. Accepts a string name (resolved via the theme's `iconResolver`, for example `mdiResolver`), a pre-rendered element, or a render function that receives `{ size, color }`.
trailingIconIconSource-NoIcon rendered after the label. Accepts a string name (resolved via the theme's `iconResolver`, for example `mdiResolver`), a pre-rendered element, or a render function that receives `{ size, color }`.
iconSizenumber-NoSize of leading and trailing icons in dp. Used when resolving string icon names or invoking the render-function form. Pre-rendered elements are not resized. Defaults to the icon size for the current `size` (20 for `extraSmall` and `small`, 24 for `medium`, 32 for `large`, 40 for `extraLarge`).
containerColorstring-NoOverride the container (background) color. State-layer colors (hover, press) are derived automatically.
contentColorstring-NoOverride the content (label and icon) color. State-layer colors are derived automatically when no containerColor is set.
labelStyleStyleProp<TextStyle>-NoAdditional style applied to the label text.
loadingbooleanNoShow an indeterminate `CircularProgress` in place of `leadingIcon`, at the icon size and in the label color. The button keeps its colors and its focus, ignores `onPress` and `onLongPress`, shows no hover or press state, and sets `aria-busy`. MD3 has no loading button, so this is a library addition. With no `leadingIcon`, the spinner adds the icon and its gap, so the button gets wider while it loads. Give it a `width` in `style` to keep the size. Pass `disabled` as well if the button must also look disabled.
styleStyleProp<ViewStyle>-NoStyle for the component. The keys that place or size it in its parent — `alignSelf`, `flex`, `flexGrow`, `flexShrink`, `flexBasis`, `width`, the margins, `position`, the insets and `zIndex` — go to the outer wrapper, so the focus ring and the shadow move with it. `minWidth` and `maxWidth` go with a `width`; alone, they style the container. The wrapper sets no `alignSelf`, so the parent's `alignItems` places the component. In a parent that stretches its children, the component keeps its own width at the start edge. Every other key styles the container. Static form only — the function form `(state) => style` is not supported because the component drives its container background through Reanimated. Use `containerColor` / `contentColor` for state-aware styling.
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: PressableKeyDownEvent) => void-No-