Skip to main content

Snackbar

Snackbars report the outcome of an action in a brief message at the bottom of the screen. Follows the Material Design 3 Snackbar specification.

Snackbar is imperative only. There is no <Snackbar visible> component, because the hard part of a snackbar is the queue — one message at a time, in order, each with its own timer — and a declarative API pushes that onto you. SnackbarProvider owns the queue; useSnackbar() gives you show, hide, and clear.

Setup

Mount SnackbarProvider once, inside ThemeProvider and inside your app's PortalHost. Snackbars render into the PORTAL_LAYERS.snackbar layer, which puts them above dialogs and bottom sheets.

import { Button, PortalHost, SnackbarProvider, useSnackbar } from '@rootnative/components'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

function Screen() {
const snackbar = useSnackbar()
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button onPress={() => snackbar.show({ message: 'Photo saved' })}>
Save photo
</Button>
</View>
)
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<PortalHost>
<SnackbarProvider>
<Screen />
</SnackbarProvider>
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

useSnackbar() throws if there is no provider above it — a silent no-op would look like a bug in your own code.

Actions

A snackbar takes a single action. Pressing it runs onAction and then dismisses with reason 'action'.

import { Button, PortalHost, SnackbarProvider, useSnackbar } from '@rootnative/components'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { View } from 'react-native'

function Screen() {
const snackbar = useSnackbar()
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button
variant="tonal"
onPress={() =>
snackbar.show({
message: 'Message deleted',
actionLabel: 'Undo',
onAction: () => console.log('undo'),
})
}
>
Delete message
</Button>
</View>
)
}

export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider>
<PortalHost>
<SnackbarProvider>
<Screen />
</SnackbarProvider>
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}

Duration

durationMeaning
'short'4s — the MD3 default
'long'10s
'indefinite'Stays until the action, the close button, or hide()
numberMilliseconds

A snackbar with an actionLabel defaults to 'indefinite', not 'short'. An undo the user never sees is worse than one that waits. A snackbar without an action defaults to 'short'.

Set showCloseIcon to add a trailing close button. RootNative warns in development if an indefinite snackbar has neither an action nor a close button — that combination is undismissable and stalls everything queued behind it.

Queueing

One snackbar is visible at a time. show() while one is up queues the new message behind it and returns an id; the queue drains in FIFO order as each snackbar's timer expires.

const first = snackbar.show({ message: 'First' })
snackbar.show({ message: 'Second' }) // waits its turn

snackbar.hide(first) // dismiss a specific one, visible or queued
snackbar.hide() // dismiss whatever is visible
snackbar.clear() // dismiss the visible one and drop the queue

Pass replace: true to jump ahead of the visible snackbar instead of queueing. The one it replaces dismisses with reason 'replaced'.

onDismiss receives why the snackbar went away: 'timeout', 'action', 'close', 'replaced', or 'manual'.

Positioning

The snackbar anchors to the bottom of the screen with 16dp margins and respects the safe-area bottom inset. Both are applied by the layer itself, so an offset you pass covers only what sits on top of them — a FAB or a bottom navigation bar.

snackbarOffsetFor(height) does that arithmetic, and FAB_SIZES gives the height:

import { FAB_SIZES, snackbarOffsetFor } from '@rootnative/components'

snackbarOffsetFor(FAB_SIZES.medium) // 56 + 16 = 72

Hard-coding the number is the trap this replaces. A literal 88 double-counts the 16dp margin the layer already added and pushes the snackbar 32dp too high.

App-wide vs per-screen

Use the provider's bottomOffset prop when every screen carries the same bottom furniture:

<SnackbarProvider bottomOffset={snackbarOffsetFor(FAB_SIZES.medium)}>
{children}
</SnackbarProvider>

When only some screens have a FAB, raise the offset from the screen that owns it. useSnackbarOffset applies while its component is mounted and reverts on unmount, so the constant lives next to the component that determines it rather than in the app root two files away:

import { FAB, FAB_SIZES, snackbarOffsetFor, useSnackbarOffset } from '@rootnative/components'

function ComposeScreen() {
useSnackbarOffset(snackbarOffsetFor(FAB_SIZES.medium))
return <FAB icon="plus" accessibilityLabel="Compose" onPress={compose} />
}

A mounted useSnackbarOffset wins over the provider's prop. With several mounted — which happens during a navigation transition, while the outgoing screen has not unmounted yet — the largest applies, so the snackbar clears every FAB on screen. Passing 0 is meaningful: it overrides the prop down to zero for that screen.

The layer is pointerEvents="box-none", so a visible snackbar never blocks the UI behind it.

Tokens

TokenValue
ContainerinverseSurface, corner 4dp (cornerExtraSmall), elevation level 3
MessagebodyMedium / inverseOnSurface
Action labellabelLarge / inversePrimary
Close icon24dp / inverseOnSurface
Height48dp single-line, 68dp two-line
WidthFull width minus 16dp margins, capped at 600dp

Per-snackbar containerColor, contentColor, and actionColor override the defaults — useful for an error-styled message.

Motion

Snackbars rise 24dp and fade in on springDefaultSpatial, and reverse on the way out. Both collapse to a hard cut under reduced motion — see Motion.

Accessibility

The surface reports role="alert" with accessibilityLiveRegion="polite", so screen readers announce the message without stealing focus. The close button is labelled "Dismiss" — override with closeAccessibilityLabel.

SnackbarProvider Props

PropTypeDefaultRequiredDescription
bottomOffsetnumber0NoExtra space below the snackbar, on top of the safe-area bottom inset. Set this to clear a FAB or a bottom navigation bar.
styleStyleProp<ViewStyle>-NoStyle applied to the snackbar surface.

show() options

SnackbarOptions is a plain object, not a component, so it is documented here rather than generated from the source.

OptionTypeDefaultDescription
messagestringThe message. Required.
actionLabelstringLabel for the single trailing action.
onAction() => voidRuns when the action is pressed; the snackbar then dismisses.
duration'short' | 'long' | 'indefinite' | number'short', or 'indefinite' with an actionLabelVisible duration.
showCloseIconbooleanfalseShow a trailing close button.
closeIconIconSource'close'Icon for the close button.
closeAccessibilityLabelstring'Dismiss'Screen-reader label for the close button.
onDismiss(reason) => voidCalled with 'timeout', 'action', 'close', 'replaced', or 'manual'.
containerColorstringinverseSurfaceOverride the container color.
contentColorstringinverseOnSurfaceOverride the message color.
actionColorstringinversePrimaryOverride the action label color.
replacebooleanfalseReplace the visible snackbar instead of queueing.

useSnackbar()

MethodSignatureDescription
show(options: SnackbarOptions) => SnackbarIdEnqueue a snackbar; returns its id.
hide(id?: SnackbarId) => voidDismiss the visible snackbar, or a specific one — visible or queued.
clear() => voidDismiss the visible snackbar and drop the queue.

The returned object is referentially stable, so it is safe in dependency arrays.