Communication
Snackbar
A brief message at the bottom of the window about something the app did, with an optional action.
- COMPONENT_SPEC §32
- CSS · custom elements · React
- Open in the playground
Examples
Snackbars from a queue
The host shows one snackbar at a time at the bottom of the window; showSnackbar resolves with how it ended. Hover or focus pauses the timer.
The live demo runs with JavaScript. Its code is below.
import { Button, SnackbarHost, useSnackbarHostState } from '@kyralabs/m3exp-web/react';
export default function Snackbars() {
const snackbars = useSnackbarHostState();
return (
<>
<Button
variant="tonal"
onPress={() => {
void snackbars.showSnackbar('Photo saved');
}}
>
Show a snackbar
</Button>
<Button
variant="outlined"
onPress={() => {
void snackbars
.showSnackbar({
message: 'Message archived',
actionLabel: 'Undo',
withDismissAction: true,
})
.then((result) => {
console.log(result); // 'action-performed' | 'dismissed'
});
}}
>
With an action
</Button>
<SnackbarHost hostState={snackbars} />
</>
);
}
<button type="button" class="m3e-button m3e-button--tonal" id="show-saved">Show a snackbar</button>
<button type="button" class="m3e-button m3e-button--outlined" id="show-undo">With an action</button>
<m3e-snackbar-host id="snackbars"></m3e-snackbar-host>
<script type="module">
const host = document.getElementById('snackbars');
document.getElementById('show-saved').addEventListener('click', () => {
host.showSnackbar('Photo saved');
});
document.getElementById('show-undo').addEventListener('click', async () => {
const result = await host.showSnackbar({
message: 'Message archived',
actionLabel: 'Undo',
withDismissAction: true,
});
console.log(result); // 'action-performed' | 'dismissed'
});
</script>
Run the HTML version (custom elements, no framework).
API
React
<Snackbar>
Snackbar (COMPONENT_SPEC §32; Compose Snackbar): inverse surface, extra-small corners, the level 3 shadow; the text, then the action (inverse primary) and the dismiss button at the end, or the action under the text (actionOnNewLine). 48dp for a line of text, 68dp for two. Usually rendered by <SnackbarHost>.
| Prop | Type | Default | Description |
|---|---|---|---|
snackbarData
|
SnackbarData | undefined
|
The data from a <SnackbarHost> (Compose Snackbar(snackbarData)): its message, its action
(a text button that performs it) and its dismiss action (an icon button "Dismiss" with a
tooltip). Without it, children, action and dismissAction are rendered as given.
|
|
actionOnNewLine
|
boolean | undefined
|
The action under the text (Compose actionOnNewLine); only with an action.
|
|
dismissLabel
|
string | undefined
|
The dismiss button's name and tooltip (default "Dismiss", Compose Strings.SnackbarDismiss).
|
|
action
|
ReactNode
|
The action (a text button with className="m3e-button m3e-button--text m3e-snackbar__action").
|
|
dismissAction
|
ReactNode
|
The extra close affordance (an icon button with className="m3e-icon-button m3e-snackbar__dismiss").
|
|
children
|
ReactNode
|
The message (without snackbarData).
|
Also accepts HTML attributes not listed here.
<SnackbarHost>
Snackbar host (COMPONENT_SPEC §32; Compose SnackbarHost): shows hostState's snackbars one at a time, fixed to the bottom center of the window and raised by --m3e-comp-snackbar-host-bottom-offset (set it above a FAB). A polite live region (role="status"); the current snackbar fades and scales in, the previous one out (FadeInFadeOutWithScale), and it is dismissed after its duration, paused while hovered or focused; Escape in it dismisses it. Focus is never moved to it on its own: hostState.focus() moves it to the current snackbar's action (bind it to a shortcut of your app). Runs the m3e-elements SnackbarHostController, as <m3e-snackbar-host> does. Other props go to the host <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
hostState
required
|
SnackbarHostState
|
The queue (useSnackbarHostState()).
|
|
snackbar
|
((data: SnackbarData) => ReactNode) | undefined
|
<Snackbar snackbarData> |
Renders each snackbar (Compose's snackbar slot). Default: <Snackbar snackbarData>.
|
actionOnNewLine
|
boolean | undefined
|
Passed to the default <Snackbar>.
|
|
dismissLabel
|
string | undefined
|
Passed to the default <Snackbar>.
|
|
recommendedTimeout
|
RecommendedTimeout | undefined
|
Android's calculateRecommendedTimeoutMillis stand-in: the time to use for a duration (the
web has no accessibility timeout setting; default: the duration).
|
Also accepts HTML attributes not listed here.
useSnackbarHostState()
A SnackbarHostState for <SnackbarHost hostState> (Compose remember { SnackbarHostState() }): showSnackbar(message | { message, actionLabel, withDismissAction, duration, signal }) queues a snackbar and resolves with 'action-performed' or 'dismissed'.
SnackbarHostState()
SnackbarHostState: shows at most one snackbar at a time. showSnackbar queues the snackbar (first in, first out, as Compose's fair Mutex) and resolves once it has been addressed; currentSnackbarData is the one showing. A host (<m3e-snackbar-host>, m3e-react's <SnackbarHost>) renders it and dismisses it after its duration.
Custom elements
<m3e-snackbar-host>
import '@kyralabs/m3e-elements/snackbar-host.js';
<m3e-snackbar-host> (COMPONENT_SPEC §32; Compose SnackbarHost + SnackbarHostState). Renders the m3e-css .m3e-snackbar-host contract inside itself (the host is display: contents): a polite live region (role="status") fixed to the bottom center of the window, raised by --m3e-comp-snackbar-host-bottom-offset (set it above a FAB; owner decision M9 #21), that shows one snackbar at a time.
showSnackbar(message | { message, actionLabel, withDismissAction, duration, signal })queues a snackbar (first in, first out) and resolves with'action-performed'or'dismissed'(Compose SnackbarResult).duration:short(4s, the default without an action),long(10s),indefinite(the default with an action). The timer pauses while the pointer is over the snackbar or focus is in it; Escape in it dismisses it (owner decision M9 #20).- The action is a text button; the dismiss action an icon button "Dismiss" (
dismiss-label) with the plain tooltip Compose builds in (owner decision M9 #30;<m3e-tooltip>'s TooltipController). action-on-new-line: the action goes under the text (ComposeactionOnNewLine).focus()moves focus to the current snackbar's action (no shortcut is bound: give users one, m3.material.io);currentSnackbarData;hostState(a SnackbarHostState: share one between hosts, or drive a host from code that has no element reference);recommendedTimeout.
| Attribute | Property | Type | Notes |
|---|---|---|---|
dismiss-label
|
dismissLabel
|
string
|
|
action-on-new-line
|
actionOnNewLine
|
boolean
|
CSS
snackbar.css
@import '@kyralabs/m3e-css/snackbar.css';
Snackbar (COMPONENT_SPEC §32): a brief message at the bottom of the window, with an optional action and dismiss (close) button. CSS draws it; the m3e-elements snackbar host (<m3e-snackbar-host>, m3e-react's <SnackbarHost>) shows one at a time from its queue, springs it in and out and dismisses it after its duration.
<div class="m3e-snackbar-host" role="status">
<div class="m3e-snackbar-host__item">
<div class="m3e-snackbar">
<div class="m3e-snackbar__text">Message archived</div>
<div class="m3e-snackbar__actions"> (optional)
<button class="m3e-button m3e-button--text m3e-snackbar__action" type="button">
Undo</button>
<button class="m3e-icon-button m3e-snackbar__dismiss" type="button"
aria-label="Dismiss"><span class="m3e-icon" aria-hidden="true">close</span></button>
</div>
</div>
</div>
</div>
- .m3e-snackbar-host: fixed to the bottom center of the window, above the bottom safe-area
inset and --m3e-comp-snackbar-host-bottom-offset (raise it above a FAB: the FAB height +
--m3e-comp-snackbar-host-fab-spacing, as Scaffold places the host; owner decision M9 #21).
A polite live region (role="status"). Its items share one cell: the leaving snackbar fades
out where the new one fades in. A .m3e-snackbar also renders on its own (static specimens).
- --action-on-new-line: the action under the text, end-aligned (Compose actionOnNewLine).
- Buttons come from button.css / icon-button.css (import them as well); the action takes the
inverse primary color, the dismiss button the inverse on-surface icon color.
- JS hooks: [data-driven] on an item (the springs write --_m3e-snackbar-scale / -opacity).
Without it an item scales and fades in on the springs' linear() curves.
29 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-divider-thickness
|
1px |
--m3e-comp-minimum-interactive-component-size
|
48px |
--m3e-comp-small-icon-button-container-height
|
40px |
--m3e-comp-snackbar-action-label-text-color
|
var(--m3e-sys-color-inverse-primary) |
--m3e-comp-snackbar-closed-opacity
|
0 |
--m3e-comp-snackbar-closed-scale
|
0.8 |
--m3e-comp-snackbar-container-color
|
var(--m3e-sys-color-inverse-surface) |
--m3e-comp-snackbar-container-max-width
|
600px |
--m3e-comp-snackbar-container-shadow
|
var(--m3e-sys-elevation-level3-shadow) |
--m3e-comp-snackbar-container-shape
|
var(--m3e-sys-shape-corner-extra-small) |
--m3e-comp-snackbar-host-bottom-offset
|
0px |
--m3e-comp-snackbar-icon-color
|
var(--m3e-sys-color-inverse-on-surface) |
--m3e-comp-snackbar-margin
|
12px |
--m3e-comp-snackbar-new-line-actions-padding-bottom
|
4px |
--m3e-comp-snackbar-new-line-actions-padding-end
|
8px |
--m3e-comp-snackbar-opacity-duration
|
var(--m3e-sys-motion-spring-fast-effects-duration) |
--m3e-comp-snackbar-opacity-easing
|
var(--m3e-sys-motion-spring-fast-effects-easing) |
--m3e-comp-snackbar-padding-start
|
16px |
--m3e-comp-snackbar-scale-duration
|
var(--m3e-sys-motion-spring-fast-spatial-duration) |
--m3e-comp-snackbar-scale-easing
|
var(--m3e-sys-motion-spring-fast-spatial-easing) |
--m3e-comp-snackbar-single-line-container-height
|
48px |
--m3e-comp-snackbar-supporting-text-color
|
var(--m3e-sys-color-inverse-on-surface) |
--m3e-comp-snackbar-supporting-text-font
|
var(--m3e-sys-typescale-body-medium-font) |
--m3e-comp-snackbar-supporting-text-line-height
|
var(--m3e-sys-typescale-body-medium-line-height) |
--m3e-comp-snackbar-supporting-text-size
|
var(--m3e-sys-typescale-body-medium-size) |
--m3e-comp-snackbar-supporting-text-tracking
|
var(--m3e-sys-typescale-body-medium-tracking) |
--m3e-comp-snackbar-supporting-text-weight
|
var(--m3e-sys-typescale-body-medium-weight) |
--m3e-comp-snackbar-text-end-spacing
|
8px |
--m3e-comp-snackbar-text-padding-vertical
|
14px |
Accessibility
the host is a polite live region, role="status" (Compose: liveRegion = Polite on the visible snackbar; m3.material.io: polite). The dismiss button is named "Dismiss" (dismiss-label; Compose Strings.SnackbarDismiss); its tooltip repeats the name, so it is aria-hidden (owner decision M9 #2). Compose's paneTitle "Alert" has no web equivalent and is not rendered.
SnackbarHostState: showSnackbar queues (first in, first out, Compose's fair Mutex) and resolves with 'action-performed' or 'dismissed'; an AbortSignal removes a call from the queue or the screen (Compose: cancelling the caller). The host dismisses the current snackbar after its duration, paused while the pointer is over it or focus is in it (owner decision M9 #20, kept by D19 (14), SOURCES #75: the web stand-in for Android's calculateRecommendedTimeoutMillis; apps that keep a timeout preference pass recommendedTimeout; the docs page notes WCAG 2.2.1 and m3.material.io's web requirement, Snackbar [2.6.0]: inline feedback or an action); Escape in it dismisses it (m3.material.io); focus never moves to it on its own; focus() (element) / hostState.focus() moves focus to its action (no shortcut is bound: apps provide one, m3.material.io); when a focused snackbar leaves, focus returns to where it came from.
Every interactive part has a target of at least 48 × 48 px, a visible focus ring for
keyboard focus only, and works under forced-colors and
prefers-reduced-motion.
Differences from Compose
the flag-on layout (owner decision M9 #19); the timer pauses while hovered or focused, which Compose does not do (#20); m3.material.io's "48dp to 64dp" for two lines against the tokens' 68dp, which Compose renders and m3e ships (SOURCES #76); no Scaffold yet: the host is fixed to the window (#21); the outgoing and incoming snackbars share the host's top edge as in Compose's Box, so a shorter new snackbar moves down when the old one is removed; the action and dismiss buttons are plain m3e-css buttons in both layers (the ripple from ripple.js).
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §32 and the m3e-css sources.
Snackbar.kt, SnackbarHost.kt, Scaffold.kt, ScaffoldSamples.kt, ComposeMaterial3Flags.kt, SnackbarTokens.kt, material3 res strings.xml L28-31 (vendored); m3.material.io Snackbar (Guidelines, Accessibility; token DB md.comp.snackbar).
SnackbarTokens.kt@19991f2c (generated snackbar.css) as read by Snackbar.kt (SnackbarDefaults: container inverse surface, extra-small corners, text inverse on-surface body medium, action inverse primary label large, dismiss icon inverse on-surface 24dp; the Surface's shadowElevation = ContainerElevation Level3), plus components/snackbar-defaults.css (PARITY.md lines):
snackbar.css sources
(compose/material3 @ 19991f2c; values: snackbar.css, snackbar-defaults.css, lines in PARITY.md):
- Snackbar.kt Snackbar (L101-168): a Surface in SnackbarDefaults' shape and colors with the ContainerElevation shadow; ComposeMaterial3Flags.isSnackbarStylingFixEnabled picks the layout (owner decision M9 #19: the flag-on branch, although the pin ships it off). OneRowSnackbar (L495-574): the text padded SnackbarVerticalPadding, the action then the dismiss button at the end, all centered vertically, at least SingleLineContainerHeight; the row is min(available, ContainerMaxWidth) inside a HorizontalSpacing start padding and, without a dismiss button, TextEndExtraSpacing at the end and between the text and the action. NewLineButtonSnackbar (L286-331): at most ContainerMaxWidth, the text padded SnackbarVerticalPadding and HorizontalSpacing at its end, the buttons end-aligned below with ActionButtonBottomPadding and, without a dismiss button, HorizontalSpacingButtonSide at the end. Snackbar(snackbarData) (L222-283): the TextButton action, the IconButton(Close) dismiss action in a TooltipBox, 12dp around it; buttons lay out at the minimum interactive size.
- SnackbarHost.kt FadeInFadeOutWithScale (L322-384): alpha on FastEffects, scale from 0.8 on FastSpatial, around the center; the items stacked in one Box.
- Scaffold.kt (L234-288): the host centered, above the FAB / bottom bar / bottom inset.