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 { mdiResolver } from '@rootnative/components/mdi'
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 iconResolver={mdiResolver}>
<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 { mdiResolver } from '@rootnative/components/mdi'
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 iconResolver={mdiResolver}>
<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'.

The clock pauses while a pointer is over the snackbar and continues from where it stopped when the pointer leaves, so a message is not pulled away from a reader who moved to it.

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 TalkBack and web screen readers announce the message without stealing focus. iOS has no live region, so the snackbar also calls AccessibilityInfo.announceForAccessibility for VoiceOver when it mounts and when a replacement arrives. 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
messagestring—The message. Required.
actionLabelstring—Label for the single trailing action.
onAction() => void—Runs 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) => void—Called 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.