Skip to content
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
Getting startedWebReactSwiftUIJetpack ComposeFlutterReact NativePresetsYour own design systemUsing Tanrei with AI agentsBrandDesign tokensDesign systems and customizationTheme StudioColorTypographyIconsShapeElevationMotionInteraction statesBehaviourLayoutAccessibilityWordsColor tokensTypescale tokensShape tokensElevation tokensMotion tokensState tokensSpace and measure tokensComponentsButtonMain action buttonSegmented buttonButton groupButton with menuToolbarCopy buttonCheckboxRadio buttonSwitchChipMenuSliderSlider fieldCalendarDateTimePickerField groupRadio groupCheckbox groupSelectSelect fieldMenubarFile uploadFile picker AdvancedDrop zoneText input AdvancedText fieldChip fieldFieldCombobox AdvancedCombobox fieldNumber input AdvancedNumber fieldColor input AdvancedColor fieldOne-time-code input AdvancedOne-time-code fieldEmbedded text input AdvancedChat composerCardDividerModal layer AdvancedDialogListList rowBottom sheetSide sheetCarouselCode blockTablePopoverStepsAccordionDescription listFigureApp shellPage headerTree viewTimelineImage previewHandleResizable frameScroll areaBadgeProgress indicatorMeterNumber tickerLoading indicatorSnackbarTooltipKeyboard shortcutTagInline codeCalloutAvatarEmpty stateTag groupSkeletonFile itemSwatchMarquee textTabsTab viewApp barNavigation barNavigation drawerSearchLinkPaginationPrevious and next linksStepperBreadcrumbsTable of contentsLink cardFAB menuStatusHow Tanrei is builtContributingLicense and attribution
HomeComponentsSnackbar

Snackbar

Tells a person briefly, at the bottom edge of the screen or at its top end, what the app just did, with at most one action, without taking focus or interrupting their work.

content_copyView as Markdown
Category
Communication
Tokens
comp.snackbar
Level
Base

Overview

Web
tanrei-snackbar
schedule
SwiftUI
TanreiSnackbar
schedule
Jetpack Compose
TanreiSnackbar
schedule
Flutter
TanreiSnackbar
schedule
React Native
Snackbar
schedule

Generated from docs/status/status.json, specs/snackbar.spec.json

A revised spec, not yet on every platform

This page describes the snackbar as the spec has it now: a bar drawn on a surface of the scheme or inverse, a quiet tonal action, status forms in quiet tints, a title, and text that expands. The web draws all of it. It stays marked lagging above until its inverse status forms have been reviewed and their baselines recorded. SwiftUI, Compose, Flutter and React Native ship an earlier revision, with a text action, no status forms, no title, no expansion and no inverse, and their samples show that revision until they catch up. The demos are the web component as it ships today.

Drawn in place: a message with an action, and one with a close button

Usage

You don’t place a snackbar. You ask a host to show one, and the host queues it, places it at the bottom of its area or at its top end, times it out and announces it. show resolves once that snackbar has closed: with action when its action was activated, or with dismiss and the reason. So “undo” code reads from top to bottom.

Platform The host Showing a snackbar
Web <tanrei-snackbar-host>, placed as the last element of the page host.show(options), a promise
SwiftUI a TanreiSnackbarHost, attached with .tanreiSnackbarHost(_:) await host.show(message:…)
Compose TanreiSnackbarHost(hostState), last in a Box with the content hostState.showSnackbar(message, …), a suspending call
Flutter TanreiSnackbarMessenger, around the page it reports on TanreiSnackbarMessenger.of(context).show(TanreiSnackbar(…)), a controller whose closed is a future
React Native rendered by TanreiProvider in its overlay layer useSnackbar().show(options), a promise

Each host also closes the snackbar that is showing (dismissCurrent) and drops the waiting ones as well (clear), both with the reason host. A snackbar element drawn outside a host is drawn in place, centred across the width it is given as a host centres it (or at the end of that width when its placement is top-end): no timeout, queue, entrance or announcement. That form is for documentation pages and tests, like the demo above.

<!-- Last on the page: the host's place in the focus order is where the snackbar's controls are. -->
<tanrei-snackbar-host></tanrei-snackbar-host>

<script type="module">
  import '@tanrei/web/components/snackbar';
  const snackbars = document.querySelector('tanrei-snackbar-host');

  async function archive() {
    await archiveConversation();
    // Resolves once it has closed: { result: 'action' } or { result: 'dismiss', reason }.
    const { result } = await snackbars.show({ message: 'Conversation archived', action: 'Undo' });
    if (result === 'action') await restoreConversation();
  }

  // A status form: the glyph, and the status in words for assistive technology.
  snackbars.show({
    status: 'error',
    statusLabel: 'Error',
    title: 'Message not sent',
    message: 'You are offline. The message will stay in Outbox.',
    action: 'Retry',
    expandLabel: 'Show full message',
  });

  // Drawn inverse, whatever the design system chooses. Leave `inverse` out to take the system's choice.
  snackbars.show({ message: 'Link copied', inverse: true });
</script>
import { useRef } from 'react';
import { SnackbarHost } from '@tanrei/react';
import type { TanreiSnackbarHost } from '@tanrei/web';

// The ref is the element: show() is its method.
const snackbars = useRef<TanreiSnackbarHost>(null);

async function archive() {
  await archiveConversation();
  const { result } = await snackbars.current!.show({ message: 'Conversation archived', action: 'Undo' });
  if (result === 'action') await restoreConversation();
}

// A status form, with a title above the message.
snackbars.current?.show({ status: 'error', statusLabel: 'Error', title: 'Message not sent', message: 'You are offline.', action: 'Retry' });

// Drawn inverse, whatever the design system chooses.
snackbars.current?.show({ message: 'Link copied', inverse: true });

// Last in the page, after the content.
<SnackbarHost ref={snackbars} />
import Tanrei

@State private var snackbars = TanreiSnackbarHost()

// Attach the host to the view whose bottom edge it uses. It draws in an overlay, not a sheet.
ContentView()
    .tanreiSnackbarHost(snackbars)

// Returns once the snackbar has closed: .action, or .dismissed(reason).
// A request to a host whose view is not on screen waits until it is.
let result = await snackbars.show(message: "Conversation archived", actionLabel: "Undo")
if case .action = result { restoreConversation() }
import io.github.chnotchy.tanrei.components.TanreiSnackbarHost
import io.github.chnotchy.tanrei.components.TanreiSnackbarHostState
import io.github.chnotchy.tanrei.components.TanreiSnackbarResult

// As Material's SnackbarHostState: showSnackbar suspends until the snackbar has closed.
val snackbars = remember { TanreiSnackbarHostState() }
val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize()) {
    Content(onArchive = {
        scope.launch {
            when (snackbars.showSnackbar("Conversation archived", actionLabel = "Undo")) {
                TanreiSnackbarResult.ActionPerformed -> unarchive()
                is TanreiSnackbarResult.Dismissed -> Unit
            }
        }
    })
    // Last in the Box: drawn above the content, and last in the focus order.
    TanreiSnackbarHost(snackbars, Modifier.fillMaxSize())
}
import 'package:tanrei/tanrei.dart';

// As ScaffoldMessenger. Put it around the page, inside its route, so a dialog opened above covers it.
TanreiSnackbarMessenger(child: page);

Future<void> archive(BuildContext context) async {
  await archiveConversation();
  final TanreiSnackbarController snackbar = TanreiSnackbarMessenger.of(context).show(
    const TanreiSnackbar(message: 'Conversation archived', actionLabel: 'Undo'),
  );
  // Completes with action, close, escape, timeout or host.
  if (await snackbar.closed == TanreiSnackbarClosedReason.action) {
    await restoreConversation();
  }
}
import { useSnackbar } from '@tanrei/react-native';

// The host lives in TanreiProvider's overlay layer: put the provider at the root of your app.
const snackbar = useSnackbar();

async function archive() {
  await archiveConversation();
  const { result } = await snackbar.show({ message: 'Conversation archived', actionLabel: 'Undo' });
  if (result === 'action') await restoreConversation();
}

Specs

A message that wraps, and an action on its own row

Anatomy

Anatomy

Drawn over the fixture message, as the web package draws it.
Part Description
1 container The bar that holds the text and the controls, raised above the content of its host. Its color is its form's: a surface of the scheme (the default form, or a faint tint of a status) or, drawn inverse, the opposite scheme's surface or, in a status form, the bar the other theme draws for that status: deep in a light scheme, pale in a dark one.
2 outline · optional The edge of a status form: a hairline in the status's color, at the edge's opacity, around the bar where the ring of its elevation is. The default form and the inverse form draw none and keep that ring alone.
3 icon · optional The status glyph from the system's icon set, before the text: info, warning, error or check_circle. Drawn only in a status form, a little softer than the text (in the status's own color on a surface and, on an inverse bar, in the status color of the other theme, at the glyph's opacity), and hidden from assistive technology, which hears statusLabel instead.
4 title · optional A few words that name what happened, above the message. Up to three lines while collapsed.
5 supporting-text The message: what happened, or with a title the description, which is then drawn a little quieter than the title. Up to three lines while collapsed.
6 action · optional One control that undoes or follows up on what happened, drawn as a tonal button with the snackbar's own tokens, as tall as the small Buttons beside it: the color of its own bar with a tonal layer of its label's color over it, so that it stands one quiet step off the bar, the same step in every form.
7 expand · optional An icon-only Button of the small size that shows the whole title and message, and hides them again: the glyph expand_more while collapsed and expand_less while expanded. Drawn only while the text is longer than its collapsed limit, and drawn as the close button is (SNK-34).
8 close · optional An icon-only Button of the small size showing the icon set's close glyph, which closes the snackbar. The snackbar places it, chooses its variant (comp.snackbar.close-button.variant: tonal in the default system) and gives it its form's tone (BTN-45): the glyph in the bar's message color and, where the variant draws a container, a circle of that color made translucent. Everything else it reads from Button's own tokens.
9 focus-ring · optional Gap-then-ring indicator shown for keyboard focus of the action, or of the text while it scrolls, in the color its form gives it so that it shows on that form's bar. The expand and close buttons draw their own, in the same color (SNK-34).

Measurements

Measurements

Length Token Value (px)
container padding-inline-start tanrei.comp.snackbar.container.padding-inline-start 16
container padding-inline-end tanrei.comp.snackbar.container.padding-inline-end 16
Drawn over the fixture message, as the web package draws it.

Properties

Property Type Default Description
message string "" What happened; with a title, the description. Required. Plain text: the whole message is announced, even when a collapsed snackbar cuts it.
title string "" A few words above the message that name what happened ("Message not sent"). Empty draws the message alone.
action string "" The label of the one action ("Undo", "View"), drawn as a tonal button that sits one quiet step off the bar, the same step in every form. Empty draws no action.
status default | info | warning | error | success "default" The form of the snackbar. Default is the neutral bar with no glyph; info, warning, error and success draw the bar as a quiet tint of that status (pale on the scheme's surface or, drawn inverse, the bar the other theme draws for that status, with that theme's text, glyph and edge colors), with an edge in the status's color and the status's glyph before the text, and warning and error are announced assertively.
statusLabel string "" The status in words, in the product's language ("Error"), announced before the title and the message so that the status never rests on color and glyph alone. Required while status is not default.
inverse boolean null Whether this snackbar is drawn inverse: in the opposite scheme's colors (a dark bar in a light scheme), so that it stands apart from whatever it lands on. False draws it on a surface of the scheme. Null, the default, takes comp.snackbar.container.inverse, the system's choice, which is off in the default system. It changes colors only (SNK-30).
closable boolean false Draws a close button, for a message that stays until the person closes it or for one they may want gone sooner.
closeLabel string "" The close button's accessible name, in the product's language ("Close"). Required while closable is true.
expanded boolean false Whether the whole title and message show. The person toggles it with the expand button (SNK-24); it has no effect while the text fits within its collapsed limits.
expandLabel string "" The expand button's accessible name, in the product's language ("Show full message"). It stays the same while expanded; the button's expanded state says which way it acts. Required whenever the text can be longer than its collapsed limit.
duration auto | short | long | indefinite "auto" How long the snackbar stays by itself. Auto is short without an action and indefinite with one (WCAG 2.2.1: a person must not be timed out of an action). Short and long close after the snackbar's timeout tokens; indefinite stays until it is closed.
placement bottom | top-end "bottom" Where the host places the snackbar in its area. Bottom, the default, is the bottom edge, centred (SNK-13). Top-end is the top edge at the end side: the top right in a left-to-right layout and the top left in a right-to-left one, and across the top of a compact window (SNK-35). A host may be given one placement for all the snackbars it shows; a snackbar that names its own keeps it. It changes where the snackbar stands and the side it enters from (SNK-36, SNK-37), and nothing else: not its size, its form, its order in the queue, its place in the focus order or its announcement.

Events

Event Description
action The action was activated. The snackbar closes.
dismiss The snackbar closed without its action; carries the reason: close (the close button), escape, timeout, or host (the product closed it or cleared the queue).
toggle The person expanded or collapsed the text with the expand button; carries the new expanded value. Not emitted when the product sets expanded.

States

Behavior

Requirements in EARS form. Every platform implementation is tested against these ids.

ID Requirement
SNK-1 WHEN the user activates the action by pointer, touch, Enter or Space THE snackbar SHALL emit action exactly once and close; WHEN the user activates the close button the same way THE snackbar SHALL emit dismiss with the reason close exactly once and close.
SNK-2 WHILE the action is hovered, keyboard-focused or pressed THE snackbar SHALL render the action's container, content color and opacity from the component tokens of the highest-precedence active state of the action, in the snackbar's form (SNK-21, SNK-30), with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. The expand and close buttons answer their states as any Button does (SNK-34).
SNK-3 WHEN the action receives keyboard focus THE snackbar SHALL show the focus ring around it without animation; focus gained by pointer or touch SHALL NOT show it. THE snackbar SHALL draw the outer stroke of that indicator, of the indicator inside the text while it scrolls and has keyboard focus (SNK-25) and, through the tone it gives them (SNK-34), of the indicators of the expand and close buttons, in its form's focus-ring color: comp.snackbar.focus-ring.color in the default form, and the token of that name under the form's prefix in every other (comp.snackbar.info.focus-ring.color, comp.snackbar.inverse.focus-ring.color, comp.snackbar.inverse-info.focus-ring.color, and warning's, error's and success's of the same names), never the system's focus-ring color role read directly. The widths and the offset SHALL stay the system's (sys.focus.outer-width, sys.focus.inner-width, sys.focus.offset), as SHALL the color of an inner stroke where the system draws one; under the glow focus style (SNK-7) the form's color SHALL be the halo's outer color.
SNK-4 WHERE the visual size of the action is below the minimum target THE snackbar SHALL extend the action's hit area on both axes to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; a platform that cannot tell the input type SHALL use sys.target.touch.
SNK-5 WHEN the action is pressed by pointer, touch, Enter or Space THE snackbar SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
SNK-6 WHILE the action is pressed THE snackbar SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container of the action at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole action and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; content color, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.snackbar.motion.feedback-duration with comp.snackbar.motion.state-easing, fades over comp.snackbar.motion.state-duration with comp.snackbar.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
SNK-7 THE snackbar SHALL ease its container corners and the action's corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
SNK-8 WHILE duration is auto THE snackbar SHALL behave as short when action is empty and as indefinite when action is set. WHILE duration is short or long THE snackbar SHALL close by itself once it has been shown for comp.snackbar.timeout.short or comp.snackbar.timeout.long, and emit dismiss with the reason timeout; WHERE the platform recommends a longer time for the person's accessibility settings (Android's accessibility timeout) it SHALL use that time, and WHERE the platform reports that a screen reader is running a snackbar with an action SHALL NOT close by itself. WHILE duration is indefinite THE snackbar SHALL NOT close by itself.
SNK-9 WHILE the pointer is over the snackbar, focus is inside it, it is expanded, the app's window is hidden or in the background, or a Tanrei modal dialog covers its host THE snackbar SHALL pause its timeout, and WHEN none of these holds any longer SHALL start the timeout again from its whole length.
SNK-10 WHEN a snackbar is shown THE snackbar SHALL have assistive technology announce it once: statusLabel first when status is not default, then the title when it has one, then the whole message (never only the part a collapsed snackbar shows), then the action's label when it has one. It SHALL announce politely (after what is being read) when status is default, info or success, and assertively (interrupting what is being read) when status is warning or error, and SHALL NOT move keyboard or screen-reader focus.
SNK-11 THE snackbar's controls SHALL be reachable in the focus order at the place its host has in that order, in this order whatever row each is drawn on: the text while it scrolls (SNK-25), the action, the expand button, the close button; WHEN the user presses Escape while focus is inside the snackbar THE snackbar SHALL emit dismiss with the reason escape and close; WHEN a snackbar closes while focus is inside it, focus SHALL return to the element that had it before focus entered the snackbar or, where that element no longer exists, to the platform's default.
SNK-12 THE host SHALL show one snackbar at a time, in the order they were requested: WHEN a snackbar is requested while another is showing or closing it SHALL wait until the other has closed, and WHILE a Tanrei modal dialog covers the host a snackbar not yet shown SHALL wait until the dialog closes. Each request SHALL resolve once its snackbar has closed, with action or with dismiss and its reason. WHEN the product closes the showing snackbar THE host SHALL close it and resolve it with dismiss and the reason host, and the next waiting one SHALL then show; WHEN the product clears the queue THE host SHALL also drop every waiting one and resolve each with dismiss and the reason host.
SNK-13 WHILE placement is bottom THE host SHALL place the snackbar at the bottom of its area, centred, comp.snackbar.container.margin above the bottom edge and at least comp.snackbar.container.margin from the side edges, inside the safe area and above the on-screen keyboard; at either placement it SHALL draw the snackbar above its area's content and below a Tanrei modal dialog and its scrim.
SNK-14 THE snackbar SHALL be at least comp.snackbar.container.min-height tall and as wide as its content between comp.snackbar.container.min-width and comp.snackbar.container.max-width, but never wider than its host less comp.snackbar.container.margin on each side. It SHALL draw comp.snackbar.container.color, or its form's container color (SNK-21, SNK-30), with comp.snackbar.container.elevation and comp.snackbar.container.shape corners, and pad its content by comp.snackbar.container.padding-inline-start before the status glyph or, without one, before the text, by comp.snackbar.container.padding-block below the text and by at least as much above it, where SNK-29 places the first line, and after the text by comp.snackbar.container.padding-inline-end when there is no control or by comp.snackbar.container.padding-inline-end-with-controls after the last control. WHILE the action stands on its own row (SNK-16) and no control follows the text on its row, the text SHALL end comp.snackbar.container.padding-inline-end from the end edge, as in a snackbar without controls, and padding-inline-end-with-controls SHALL be the action row's alone.
SNK-15 THE snackbar SHALL draw the message in comp.snackbar.supporting-text.typescale and comp.snackbar.supporting-text.color, or its form's message color (SNK-21, SNK-30), at the opacity SNK-32 gives a message under a title, followed comp.snackbar.container.gap after the text (the title and the message) by the action, the expand button and the close button, in that order, comp.snackbar.controls.gap apart and centred on the text's first line (SNK-29). The action SHALL be drawn as a tonal button, in its form's action colors: a container of comp.snackbar.action.container.color at least comp.snackbar.controls.height tall (the height of a small Button, so that the action stands as tall as the expand and close buttons in its row), the tonal layer over it (SNK-33), and comp.snackbar.controls.padding-inline around its label in comp.snackbar.controls.label.typescale and comp.snackbar.action.content.color, on comp.snackbar.controls.shape corners. The expand and close buttons SHALL be drawn as SNK-34 says.
SNK-16 WHEN the text (the title and the message) would need more than two lines beside the controls, or the action's label would itself wrap, THE snackbar SHALL move the action onto its own row below the text, comp.snackbar.container.gap below it and aligned to the end, with comp.snackbar.container.padding-inline-end-with-controls between that row and the bar's bottom edge, while the expand and close buttons stay at the end of the first row; the text SHALL wrap, and is cut only as SNK-23 says, and the action's label SHALL never wrap or be truncated.
SNK-17 WHEN a snackbar is shown THE snackbar SHALL rise into place from below the bottom edge of its host by its own height plus its margin while fading in (WHILE placement is bottom; at top-end it descends instead, SNK-36), over comp.snackbar.motion.enter-duration with comp.snackbar.motion.enter-easing; WHEN it closes it SHALL fade out in place over comp.snackbar.motion.exit-duration with comp.snackbar.motion.exit-easing, and the host SHALL show the next one only after that. WHERE motion is reduced it SHALL fade in and out without moving, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's).
SNK-18 WHEN the layout direction is right-to-left THE snackbar SHALL mirror its layout (the status glyph and the text at the right, the controls at the left) without mirroring any glyph, and SHALL grow a ripple from the point of contact as in left-to-right.
SNK-19 WHILE a snackbar is drawn outside a host (a snapshot harness, a documentation page) THE snackbar SHALL draw in place at its resting position, centred across the width it is given as SNK-13 centres it in a host (or, WHILE placement is top-end, at the end of that width as SNK-35 places it, and across the whole of it where that width is narrower than comp.snackbar.breakpoint.medium) and at most as tall as the height it is given, the width and the height it is given standing for the host's area in SNK-25 and SNK-35; it SHALL NOT time out, queue, animate its entrance or announce itself, and SHALL still emit action, dismiss and toggle.
SNK-20 WHILE message is empty THE snackbar SHALL still render and in debug builds report the empty message; WHILE closable is true and closeLabel is empty it SHALL still render the close button and in debug builds report that the button has no accessible name; WHILE status is not default and statusLabel is empty it SHALL still render the status form and in debug builds report that the status is not given in words; WHILE the expand button is drawn and expandLabel is empty it SHALL still render it and in debug builds report that it has no accessible name; WHEN duration is short or long and action is set it SHALL, in debug builds, remind the product that a timed action must also be offered elsewhere.
SNK-21 WHILE status is info, warning, error or success THE snackbar SHALL draw that status form: the bar, the title, the message and the status glyph in the status's container, title, supporting-text and icon colors, and every state of the action from the parts prefixed with the status, and give the expand and close buttons the status's tone (SNK-34). For info, while the snackbar is not drawn inverse (SNK-30), these are comp.snackbar.info.container.color, comp.snackbar.info.title.color, comp.snackbar.info.supporting-text.color, comp.snackbar.info.icon.color, comp.snackbar.info.outline.color for its edge (SNK-31), comp.snackbar.action.info.container.color and comp.snackbar.action.info.content.color; warning, error and success read their own tokens of the same names, and a status form drawn inverse reads the tokens SNK-30 names. Before the text it SHALL draw the status's glyph from the system's icon set (info for info, warning for warning, error for error, check_circle for success) at comp.snackbar.icon.size, in its form's icon color drawn at comp.snackbar.icon.opacity (an opacity of the glyph alone, which fades nothing else), comp.snackbar.icon.gap before the text and centred on the text's first line (SNK-29), with the family and fill of an unselected icon, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, never mirrored. The glyph so drawn SHALL keep 3:1 against its bar in every scheme of the default system. The glyph SHALL be hidden from assistive technology, and statusLabel SHALL be exposed as text before the title and the message. WHILE status is default THE snackbar SHALL draw no glyph and read the unprefixed tokens, or drawn inverse the tokens prefixed with inverse (SNK-30).
SNK-22 WHILE title is not empty THE snackbar SHALL draw it above the message in comp.snackbar.title.typescale and comp.snackbar.title.color, or its form's title color (SNK-21, SNK-30), with comp.snackbar.title.gap between the title and the message; WHILE title is empty it SHALL draw the message alone. THE snackbar SHALL set the title's letter-spacing by the script of its text, as every title is set (PRS-20): no negative letter-spacing WHERE its language is Chinese, Japanese or Korean, and none WHERE its script joins its letters or stacks marks on them.
SNK-23 WHILE expanded is false THE snackbar SHALL show at most comp.snackbar.title.max-lines lines of the title and at most comp.snackbar.supporting-text.max-lines lines of the message, each ending with an ellipsis where it is cut, at every text size, and the whole of both SHALL still reach assistive technology. WHILE the title or the message is longer than its collapsed limit THE snackbar SHALL draw the expand button, holding expand_more while collapsed and expand_less while expanded; WHILE both fit within their limits it SHALL NOT draw the expand button, and expanded SHALL have no effect. WHILE expanded is true THE snackbar SHALL show the whole title and message.
SNK-24 WHEN the user activates the expand button by pointer, touch, Enter or Space THE snackbar SHALL toggle expanded, emit toggle with the new value exactly once, keep focus on the expand button and stay open; the bar SHALL grow to its expanded height or shrink back over comp.snackbar.motion.expand-duration with comp.snackbar.motion.expand-easing, keeping its bottom edge in place WHILE placement is bottom and its top edge at top-end (SNK-37), and change at once where motion is reduced. The expand button SHALL expose to assistive technology whether the text is expanded and which text it shows and hides, and SHALL keep expandLabel as its name in both states. WHEN the product sets expanded THE snackbar SHALL change the same way without emitting toggle.
SNK-25 WHILE the snackbar is expanded and its whole content would make it taller than its host's area allows (the area's height less comp.snackbar.container.margin above and below, inside the safe area and above the on-screen keyboard) THE snackbar SHALL take that height and scroll its text inside the bar, keeping the status glyph, the controls and an action on its own row in place; WHILE the text scrolls it SHALL be reachable by keyboard (a focus stop, scrolled by the arrow keys, Page Up and Page Down), named by the title when there is one, and scrolled by wheel, trackpad and touch.
SNK-26 WHILE the system's icon set cannot yet draw a glyph of the snackbar (its font is still loading) — the status glyph, or the glyph it gives the expand or the close button — THE snackbar SHALL draw nothing in that glyph's place, never the glyph's name as text, SHALL keep the glyph's box, and SHALL draw the glyph once the set can; statusLabel, expandLabel and closeLabel SHALL still reach assistive technology.
SNK-27 THE snackbar SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the snackbar is shown THE snackbar SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state, the time left before it closes and its focus.
SNK-28 THE snackbar SHALL lay out its title, its message and its action's label as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE snackbar SHALL still place each and align its lines by the layout direction, not by the text's own, and SHALL cut a title or a message cut by a line limit that does not fit (SNK-23) at the end of its own direction, so that a left-to-right label in a right-to-left layout loses its right end and never its beginning.
SNK-29 THE snackbar SHALL align its content to the top of the bar. It SHALL centre the first line of the text (the title's, or the message's without a title) half of comp.snackbar.container.min-height below the bar's top edge, or lower where comp.snackbar.container.padding-block plus half that line's height is more (enlarged text), and SHALL centre the status glyph and every control of the first row (the action while it is beside the text, the expand button and the close button) on that line; every further line of the text follows below it, an action on its own row (SNK-16) follows the text, and any height the bar has beyond its content SHALL lie below the content, never above it. WHEN the bar expands or collapses (SNK-24), and WHEN its text is cut or shown whole (SNK-23), the status glyph, the first line of the title and of the message, and every control of the first row SHALL keep their distance from the bar's top edge, during the motion and after it; an action on its own row SHALL keep its distance from the bar's bottom edge.
SNK-30 THE snackbar SHALL be drawn inverse WHILE inverse is true, or WHILE inverse is null and comp.snackbar.container.inverse is on; otherwise (inverse false, or null where that choice is off) it SHALL be drawn on a surface of the scheme, from the unprefixed tokens or its status form's (SNK-21). WHILE it is drawn inverse and status is default THE snackbar SHALL draw the bar, the title and the message in comp.snackbar.inverse.container.color, comp.snackbar.inverse.title.color and comp.snackbar.inverse.supporting-text.color, and every state of the action from the parts prefixed with inverse (comp.snackbar.action.inverse.container.color and comp.snackbar.action.inverse.content.color), and give the expand and close buttons that form's tone (SNK-34). WHILE it is drawn inverse and status is info, warning, error or success it SHALL draw that status's inverse form the same way, from the parts prefixed with inverse- and the status: for info comp.snackbar.inverse-info.container.color, comp.snackbar.inverse-info.title.color, comp.snackbar.inverse-info.supporting-text.color, comp.snackbar.inverse-info.icon.color, comp.snackbar.inverse-info.outline.color for its edge (SNK-31), comp.snackbar.action.inverse-info.container.color and comp.snackbar.action.inverse-info.content.color, and for warning, error and success their own tokens of the same names. Under the default system each color of a status's inverse form SHALL be the color the same part of that status's form not drawn inverse has in the scheme of the other theme at the same contrast level (light for dark, light-medium for dark-medium, light-high for dark-high, and each the other way): the bar, the title, the message, the glyph, the edge, the focus indicator, every state of the action and the tone of the expand and close buttons, so that the inverse form of a status in one theme and its form on a surface in the other differ only in what lies around the bar. Being drawn inverse SHALL change colors only: the bar's elevation, corners, sizes and layout, the glyphs, the width and the opacity of a status form's edge, the opacities of the glyph, of a message under a title and of the action's tonal layer, the motion, the timing, the announcement and the keyboard behaviour SHALL be those of the same snackbar not drawn inverse. WHEN inverse or the system's choice changes while the snackbar is shown THE snackbar SHALL draw the other form at once, keeping its open state, the time left before it closes and its focus.
SNK-31 WHILE status is info, warning, error or success THE snackbar SHALL draw its form's edge: a line comp.snackbar.outline.width wide around the bar, outside the bar's bounds and following its corners, where the ring of comp.snackbar.container.elevation lies (the layer of that elevation with no offset, no blur and a positive spread), over that ring and above the elevation's shadows, in the form's outline color drawn at comp.snackbar.outline.opacity: comp.snackbar.info.outline.color for info, comp.snackbar.inverse-info.outline.color for info drawn inverse (SNK-30), and warning's, error's and success's tokens of the same names. The edge SHALL NOT change the bar's size or the place of anything inside or around it, and the focus indicator of a control SHALL be drawn above it. WHILE status is default THE snackbar SHALL draw no edge of its own, drawn inverse or not: the ring of its elevation shows alone, as on every raised surface. WHERE comp.snackbar.outline.opacity is 0 a status form SHALL draw no edge either.
SNK-32 WHILE title is not empty THE snackbar SHALL draw the message at comp.snackbar.supporting-text.under-title.opacity: an opacity of the message's text alone, applied to its form's message color (SNK-15), in every form, collapsed and expanded. WHILE title is empty it SHALL draw the message whole, and it SHALL draw the title whole in every form. The message so drawn SHALL keep 4.5:1 against its bar in every scheme of the default system, and 7:1 in its high-contrast schemes; what assistive technology reads SHALL NOT change with it.
SNK-33 THE snackbar SHALL draw the action's tonal layer in every form and in every state of the action: the action's content color of that form and state, at comp.snackbar.action.tonal-layer.opacity, filling the action's container to its corners, above the container color of that state (SNK-2) and above a ripple that grows on it (SNK-6), and under the label. The layer is the action's alone, SHALL NOT take a press or change the action's size, and SHALL NOT be drawn WHERE comp.snackbar.action.tonal-layer.opacity is 0, where the action is its container alone. The label SHALL keep 4.5:1 against the action as drawn (the container color of the state with the layer over it) in every form, state and scheme of the default system, and 7:1 in its high-contrast schemes.
SNK-34 WHILE closable is true, and WHILE the expand button is drawn (SNK-23), THE snackbar SHALL draw that button, in the place SNK-15, SNK-16 and SNK-29 give it, as an icon-only Button of the small size showing a glyph of the system's icon set (close; expand_more or expand_less, SNK-23) and named by closeLabel or expandLabel, in the variant comp.snackbar.close-button.variant names and in the tone of the snackbar's form (BTN-45): the form's message color — comp.snackbar.supporting-text.color in the default form, and the token of that name under the form's prefix in every other (SNK-21, SNK-30) — with the form's focus-ring color (SNK-3) for its focus indicator, both given through the channel a Button reads from the component that places it, and otherwise unrestyled: it reads Button's own tokens for its size, glyph size, corners and state opacities, answers hover, focus and press as any Button does, and keeps its own minimum target (BTN-5). WHEN the snackbar's form changes while it is shown THE snackbar SHALL give the buttons the new form's tone with the new form's colors (SNK-30). THE snackbar SHALL draw no close or expand mark of its own (no path, shape or character of its own), and the expand button SHALL expose to assistive technology whether the text is expanded and which text it shows and hides (SNK-24).
SNK-35 WHILE placement is top-end THE host SHALL place the snackbar at the top of its area, at its end side (the right in a left-to-right layout and the left in a right-to-left one, taken from where the host is used as SNK-27 says), comp.snackbar.container.margin below the top edge and comp.snackbar.container.margin from the end edge, inside the safe area and below what the frame keeps over the top of the area (a sticky header's present height, APS-17), so that it never covers a top app bar; WHILE the area is narrower than comp.snackbar.breakpoint.medium (a compact window) it SHALL draw the snackbar across the top of the area instead, as wide as the area less comp.snackbar.container.margin on each side, within comp.snackbar.container.max-width. WHEN the product asks for a snackbar without a placement THE host SHALL give it the placement the host itself is given, and bottom where it is given none. The placement SHALL NOT change the snackbar's layer (SNK-13), its other size bounds (SNK-14), its place in the focus order (SNK-11) or its announcement (SNK-10), and the host SHALL still show one snackbar at a time, in order (SNK-12): at the top as at the bottom, a snackbar asked for while another shows waits for it to close, and no two are stacked.
SNK-36 WHILE placement is top-end, WHEN a snackbar is shown THE snackbar SHALL descend into place from above the top edge of its host by its own height plus its margin while fading in, over comp.snackbar.motion.enter-duration with comp.snackbar.motion.enter-easing, as SNK-17 rises at the bottom; WHEN it closes it SHALL fade out in place as SNK-17 says, and WHERE motion is reduced it SHALL fade in and out without moving, or appear and disappear at once where the platform removes every animation.
SNK-37 WHILE placement is top-end, WHEN the bar expands or collapses (SNK-24) THE snackbar SHALL grow downward and shrink back with its top edge in place, so that, its content being aligned to the top (SNK-29), nothing of its first row moves on the screen during the motion or after it; WHILE it is expanded it SHALL be at most as tall as SNK-25 allows, the area's height being measured from below what the frame keeps over its top (SNK-35).

Platform name mapping

Parity does not mean identical APIs. Each platform uses its own idiom for the same contract.

Spec Web SwiftUI Jetpack Compose Flutter React Native
Component tanrei-snackbar TanreiSnackbar TanreiSnackbar TanreiSnackbar Snackbar
message · Property message message message message message
title · Property headline title title title title
action · Property action actionLabel actionLabel actionLabel actionLabel
status · Property status status status status status
statusLabel · Property statusLabel statusLabel statusLabel statusLabel statusLabel
inverse · Property inverse inverse inverse inverse inverse
closable · Property closable withCloseButton withDismissAction showCloseIcon closable
closeLabel · Property closeLabel closeLabel closeLabel closeIconLabel closeLabel
expanded · Property expanded isExpanded expanded expanded expanded
expandLabel · Property expandLabel expandLabel expandLabel expandLabel expandLabel
duration · Property duration duration duration duration duration
placement · Property placement placement placement placement placement
action · Event action TanreiSnackbarResult.action TanreiSnackbarResult.ActionPerformed TanreiSnackbarClosedReason.action onAction
dismiss · Event dismiss TanreiSnackbarResult.dismissed TanreiSnackbarResult.Dismissed TanreiSnackbarClosedReason onDismiss
toggle · Event expandedchange onExpandedChange onExpandedChange onExpandedChanged onExpandedChange
Notes title is the headline property and attribute, because HTML's global title attribute would show a tooltip over the whole bar (as the app bar's). closeLabel, statusLabel and expandLabel are the close-label, status-label and expand-label attributes. inverse is a property that is true, false or null (the default); the inverse attribute sets it: absent is null, present (empty or true) is true, and inverse="false" is false, the one way to refuse a system whose choice is on. While it is null the snackbar reads --tanrei-comp-snackbar-container-inverse from the cascade, which carries the choice's name (SNK-30). The opacities of SNK-21, SNK-31, SNK-32 and SNK-33 are applied to the color of the part they name (a mix of that color with transparent), never as an element's opacity, so nothing else fades with it; a status form's edge is a shadow layer of its own before the elevation's, and the tonal layer lies inside the action, under its label. The focus indicator's color is the form's focus-ring-color custom property, handed to the shared focus primitive (SNK-3). The expand and close buttons are <tanrei-button size="small"> elements in the snackbar's shadow root, given the variant --tanrei-comp-snackbar-close-button-variant names and the form's tone through the Button's internal channel (SNK-34); the expand button's expanded state and the text it controls are written on the Button's own native button (aria-expanded, and ariaControlsElements, since an id cannot be named across the Button's shadow root). dismiss is a CustomEvent whose detail.reason is the reason. toggle is the expandedchange CustomEvent, whose detail.expanded is the new value, because a popover element already fires a native toggle when it is shown and hidden. The host (<tanrei-snackbar-host>) queues and places snackbars; show(options) takes title, status, statusLabel, inverse and expandLabel as well; see the accessibility notes. placement is the placement attribute of <tanrei-snackbar> and of <tanrei-snackbar-host>: the host's is the placement of each snackbar whose show() options name none (SNK-35). What the frame keeps over the top of the area is --tanrei-app-shell-scroll-offset on the document's root, which <tanrei-app-shell> sets to its sticky header's height (APS-17) and a product's own frame may set; the safe area is env(safe-area-inset-*). A compact window is one narrower than --tanrei-comp-snackbar-breakpoint-medium, read by the stylesheet itself, with no listener. Snapshot harness: each fixture is a <tanrei-snackbar> rendered outside a host (SNK-19), on a 400 px wide stage; expanded-scrolls on a stage 400 × 240, and top-end and top-end-rtl on a stage 720 wide, wider than a compact window. Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. A host is a @Observable TanreiSnackbarHost attached with .tanreiSnackbarHost(_:) on the view whose bottom edge it uses; await host.show(message:actionLabel:withCloseButton:closeLabel:duration:) returns a TanreiSnackbarResult (.action, or .dismissed(reason)) once the snackbar has closed. It is drawn in an overlay of that view, not in a sheet, which would be modal. Snapshot harness: TanreiSnackbar rendered in place (SNK-19) in a 400-point frame. inverse is a Bool?, nil by default. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20). Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. Shaped like Material's snackbar API: TanreiSnackbarHost(hostState) is placed in the layout (for example a scaffold's snackbar slot), and hostState.showSnackbar(message, actionLabel, withDismissAction, closeLabel, duration) suspends until the snackbar has closed and returns the result; cancelling the coroutine closes it with the reason host. The recommended timeout is AccessibilityManager.calculateRecommendedTimeoutMillis. Snapshot harness: TanreiSnackbar(message = …) rendered in place (SNK-19) in a 400 dp wide box. inverse is a Boolean?, null by default. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20). Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. Shaped like Flutter's ScaffoldMessenger: a TanreiSnackbarMessenger placed around the page, inside its route, hosts the queue (so a Tanrei dialog's route covers it and SNK-12's wait applies), and TanreiSnackbarMessenger.of(context).show(TanreiSnackbar(...)) returns a controller whose closed future completes with a TanreiSnackbarClosedReason (action, close, escape, timeout, host). showCloseIcon is Flutter's own name for the close button. A running screen reader is MediaQuery.accessibleNavigation (as Flutter's own SnackBar reads it); Android's recommended timeout needs a platform channel, since Flutter does not expose it. Snapshot harness: a golden of TanreiSnackbar rendered in place (SNK-19) in a 400-wide surface. inverse is a bool?, null by default. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20). Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. TanreiProvider renders the host in its root overlay layer, not in a Modal (a Modal is modal and takes focus); useSnackbar().show({ message, actionLabel, closable, closeLabel, duration }) returns a promise of the result. A Snackbar element rendered directly is drawn in place (SNK-19). Snapshot harness: Snackbar rendered in place in a 400-wide view. inverse is boolean | null | undefined; both null and undefined take the system's choice. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20).

Tokens

Token Aliases Resolved (light)
--tanrei-comp-snackbar-action-container-color comp.snackbar.container.color #ffffff
--tanrei-comp-snackbar-action-content-color comp.snackbar.supporting-text.color #1b1b1b
--tanrei-comp-snackbar-action-error-container-color comp.snackbar.error.container.color #ffedeb
--tanrei-comp-snackbar-action-error-content-color comp.snackbar.error.supporting-text.color #3a211f
--tanrei-comp-snackbar-action-error-state-layer-color comp.snackbar.action.error.content.color #3a211f
--tanrei-comp-snackbar-action-focused-container-color — #e8e8e8
--tanrei-comp-snackbar-action-focused-content-color — #1b1b1b
--tanrei-comp-snackbar-action-focused-error-container-color — #ebd9d7
--tanrei-comp-snackbar-action-focused-error-content-color — #3a211f
--tanrei-comp-snackbar-action-focused-error-state-layer-color — #3a211f
--tanrei-comp-snackbar-action-focused-info-container-color — #d9dceb
--tanrei-comp-snackbar-action-focused-info-content-color — #212839
--tanrei-comp-snackbar-action-focused-info-state-layer-color — #212839
--tanrei-comp-snackbar-action-focused-inverse-error-container-color — #4a312f
--tanrei-comp-snackbar-action-focused-inverse-error-content-color — #ffe1df
--tanrei-comp-snackbar-action-focused-inverse-error-state-layer-color — #ffe1df
--tanrei-comp-snackbar-action-focused-inverse-info-container-color — #303748
--tanrei-comp-snackbar-action-focused-inverse-info-content-color — #e1e8ff
--tanrei-comp-snackbar-action-focused-inverse-info-state-layer-color — #e1e8ff
--tanrei-comp-snackbar-action-focused-inverse-success-container-color — #2b3b2b
--tanrei-comp-snackbar-action-focused-inverse-success-content-color — #daeed6
--tanrei-comp-snackbar-action-focused-inverse-success-state-layer-color — #daeed6
--tanrei-comp-snackbar-action-focused-inverse-warning-container-color — #47341f
--tanrei-comp-snackbar-action-focused-inverse-warning-content-color — #ffe3cb
--tanrei-comp-snackbar-action-focused-inverse-warning-state-layer-color — #ffe3cb
--tanrei-comp-snackbar-action-focused-inverse-container-color — #343434
--tanrei-comp-snackbar-action-focused-inverse-content-color — #f1f1f1
--tanrei-comp-snackbar-action-focused-inverse-state-layer-color — #f1f1f1
--tanrei-comp-snackbar-action-focused-state-layer-color — #1b1b1b
--tanrei-comp-snackbar-action-focused-success-container-color — #cee2cc
--tanrei-comp-snackbar-action-focused-success-content-color — #1b2c1c
--tanrei-comp-snackbar-action-focused-success-state-layer-color — #1b2c1c
--tanrei-comp-snackbar-action-focused-warning-container-color — #ebdacb
--tanrei-comp-snackbar-action-focused-warning-content-color — #362410
--tanrei-comp-snackbar-action-focused-warning-state-layer-color — #362410
--tanrei-comp-snackbar-action-hovered-container-color — #ededed
--tanrei-comp-snackbar-action-hovered-content-color — #1b1b1b
--tanrei-comp-snackbar-action-hovered-error-container-color — #efdddb
--tanrei-comp-snackbar-action-hovered-error-content-color — #3a211f
--tanrei-comp-snackbar-action-hovered-error-state-layer-color — #3a211f
--tanrei-comp-snackbar-action-hovered-info-container-color — #dde0ef
--tanrei-comp-snackbar-action-hovered-info-content-color — #212839
--tanrei-comp-snackbar-action-hovered-info-state-layer-color — #212839
--tanrei-comp-snackbar-action-hovered-inverse-error-container-color — #462d2b
--tanrei-comp-snackbar-action-hovered-inverse-error-content-color — #ffe1df
--tanrei-comp-snackbar-action-hovered-inverse-error-state-layer-color — #ffe1df
--tanrei-comp-snackbar-action-hovered-inverse-info-container-color — #2c3344
--tanrei-comp-snackbar-action-hovered-inverse-info-content-color — #e1e8ff
--tanrei-comp-snackbar-action-hovered-inverse-info-state-layer-color — #e1e8ff
--tanrei-comp-snackbar-action-hovered-inverse-success-container-color — #273727
--tanrei-comp-snackbar-action-hovered-inverse-success-content-color — #daeed6
--tanrei-comp-snackbar-action-hovered-inverse-success-state-layer-color — #daeed6
--tanrei-comp-snackbar-action-hovered-inverse-warning-container-color — #42301b
--tanrei-comp-snackbar-action-hovered-inverse-warning-content-color — #ffe3cb
--tanrei-comp-snackbar-action-hovered-inverse-warning-state-layer-color — #ffe3cb
--tanrei-comp-snackbar-action-hovered-inverse-container-color — #303030
--tanrei-comp-snackbar-action-hovered-inverse-content-color — #f1f1f1
--tanrei-comp-snackbar-action-hovered-inverse-state-layer-color — #f1f1f1
--tanrei-comp-snackbar-action-hovered-state-layer-color — #1b1b1b
--tanrei-comp-snackbar-action-hovered-success-container-color — #d2e6cf
--tanrei-comp-snackbar-action-hovered-success-content-color — #1b2c1c
--tanrei-comp-snackbar-action-hovered-success-state-layer-color — #1b2c1c
--tanrei-comp-snackbar-action-hovered-warning-container-color — #efdecf
--tanrei-comp-snackbar-action-hovered-warning-content-color — #362410
--tanrei-comp-snackbar-action-hovered-warning-state-layer-color — #362410
--tanrei-comp-snackbar-action-info-container-color comp.snackbar.info.container.color #edf0ff
--tanrei-comp-snackbar-action-info-content-color comp.snackbar.info.supporting-text.color #212839
--tanrei-comp-snackbar-action-info-state-layer-color comp.snackbar.action.info.content.color #212839
--tanrei-comp-snackbar-action-inverse-error-container-color comp.snackbar.inverse-error.container.color #361d1b
--tanrei-comp-snackbar-action-inverse-error-content-color comp.snackbar.inverse-error.supporting-text.color #ffe1df
--tanrei-comp-snackbar-action-inverse-error-state-layer-color comp.snackbar.action.inverse-error.content.color #ffe1df
--tanrei-comp-snackbar-action-inverse-info-container-color comp.snackbar.inverse-info.container.color #1c2334
--tanrei-comp-snackbar-action-inverse-info-content-color comp.snackbar.inverse-info.supporting-text.color #e1e8ff
--tanrei-comp-snackbar-action-inverse-info-state-layer-color comp.snackbar.action.inverse-info.content.color #e1e8ff
--tanrei-comp-snackbar-action-inverse-success-container-color comp.snackbar.inverse-success.container.color #172718
--tanrei-comp-snackbar-action-inverse-success-content-color comp.snackbar.inverse-success.supporting-text.color #daeed6
--tanrei-comp-snackbar-action-inverse-success-state-layer-color comp.snackbar.action.inverse-success.content.color #daeed6
--tanrei-comp-snackbar-action-inverse-warning-container-color comp.snackbar.inverse-warning.container.color #32200c
--tanrei-comp-snackbar-action-inverse-warning-content-color comp.snackbar.inverse-warning.supporting-text.color #ffe3cb
--tanrei-comp-snackbar-action-inverse-warning-state-layer-color comp.snackbar.action.inverse-warning.content.color #ffe3cb
--tanrei-comp-snackbar-action-inverse-container-color comp.snackbar.inverse.container.color #1f1f1f
--tanrei-comp-snackbar-action-inverse-content-color comp.snackbar.inverse.supporting-text.color #f1f1f1
--tanrei-comp-snackbar-action-inverse-state-layer-color comp.snackbar.action.inverse.content.color #f1f1f1
--tanrei-comp-snackbar-action-pressed-container-color — #e8e8e8
--tanrei-comp-snackbar-action-pressed-content-color — #1b1b1b
--tanrei-comp-snackbar-action-pressed-error-container-color — #ebd9d7
--tanrei-comp-snackbar-action-pressed-error-content-color — #3a211f
--tanrei-comp-snackbar-action-pressed-error-state-layer-color — #3a211f
--tanrei-comp-snackbar-action-pressed-info-container-color — #d9dceb
--tanrei-comp-snackbar-action-pressed-info-content-color — #212839
--tanrei-comp-snackbar-action-pressed-info-state-layer-color — #212839
--tanrei-comp-snackbar-action-pressed-inverse-error-container-color — #4a312f
--tanrei-comp-snackbar-action-pressed-inverse-error-content-color — #ffe1df
--tanrei-comp-snackbar-action-pressed-inverse-error-state-layer-color — #ffe1df
--tanrei-comp-snackbar-action-pressed-inverse-info-container-color — #303748
--tanrei-comp-snackbar-action-pressed-inverse-info-content-color — #e1e8ff
--tanrei-comp-snackbar-action-pressed-inverse-info-state-layer-color — #e1e8ff
--tanrei-comp-snackbar-action-pressed-inverse-success-container-color — #2b3b2b
--tanrei-comp-snackbar-action-pressed-inverse-success-content-color — #daeed6
--tanrei-comp-snackbar-action-pressed-inverse-success-state-layer-color — #daeed6
--tanrei-comp-snackbar-action-pressed-inverse-warning-container-color — #47341f
--tanrei-comp-snackbar-action-pressed-inverse-warning-content-color — #ffe3cb
--tanrei-comp-snackbar-action-pressed-inverse-warning-state-layer-color — #ffe3cb
--tanrei-comp-snackbar-action-pressed-inverse-container-color — #343434
--tanrei-comp-snackbar-action-pressed-inverse-content-color — #f1f1f1
--tanrei-comp-snackbar-action-pressed-inverse-state-layer-color — #f1f1f1
--tanrei-comp-snackbar-action-pressed-state-layer-color — #1b1b1b
--tanrei-comp-snackbar-action-pressed-success-container-color — #cee2cc
--tanrei-comp-snackbar-action-pressed-success-content-color — #1b2c1c
--tanrei-comp-snackbar-action-pressed-success-state-layer-color — #1b2c1c
--tanrei-comp-snackbar-action-pressed-warning-container-color — #ebdacb
--tanrei-comp-snackbar-action-pressed-warning-content-color — #362410
--tanrei-comp-snackbar-action-pressed-warning-state-layer-color — #362410
--tanrei-comp-snackbar-action-state-layer-color comp.snackbar.action.content.color #1b1b1b
--tanrei-comp-snackbar-action-success-container-color comp.snackbar.success.container.color #e2f6df
--tanrei-comp-snackbar-action-success-content-color comp.snackbar.success.supporting-text.color #1b2c1c
--tanrei-comp-snackbar-action-success-state-layer-color comp.snackbar.action.success.content.color #1b2c1c
--tanrei-comp-snackbar-action-tonal-layer-opacity — 0.12
--tanrei-comp-snackbar-action-warning-container-color comp.snackbar.warning.container.color #ffeee0
--tanrei-comp-snackbar-action-warning-content-color comp.snackbar.warning.supporting-text.color #362410
--tanrei-comp-snackbar-action-warning-state-layer-color comp.snackbar.action.warning.content.color #362410
--tanrei-comp-snackbar-breakpoint-medium sys.breakpoint.medium 600px
--tanrei-comp-snackbar-close-button-variant — 1
--tanrei-comp-snackbar-container-color sys.color.surface-bright #ffffff
--tanrei-comp-snackbar-container-elevation sys.elevation.level3 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-snackbar-container-gap sys.space.space100 8px
--tanrei-comp-snackbar-container-inverse — 0
--tanrei-comp-snackbar-container-margin sys.space.space200 16px
--tanrei-comp-snackbar-container-max-width — 560px
--tanrei-comp-snackbar-container-min-height — 48px
--tanrei-comp-snackbar-container-min-width — 288px
--tanrei-comp-snackbar-container-padding-block sys.space.space150 12px
--tanrei-comp-snackbar-container-padding-inline-end sys.space.space200 16px
--tanrei-comp-snackbar-container-padding-inline-end-with-controls sys.space.space100 8px
--tanrei-comp-snackbar-container-padding-inline-start sys.space.space200 16px
--tanrei-comp-snackbar-container-shape sys.shape.corner.medium 8px
--tanrei-comp-snackbar-controls-gap sys.space.space50 4px
--tanrei-comp-snackbar-controls-height comp.button.container.height.small 32px
--tanrei-comp-snackbar-controls-padding-inline sys.space.space175 14px
--tanrei-comp-snackbar-controls-shape sys.shape.corner.full 9999px
--tanrei-comp-snackbar-error-container-color sys.color.error-container #ffedeb
--tanrei-comp-snackbar-error-focus-ring-color sys.color.on-error-container #3a211f
--tanrei-comp-snackbar-error-icon-color sys.color.error #cd262f
--tanrei-comp-snackbar-error-outline-color sys.color.error #cd262f
--tanrei-comp-snackbar-error-supporting-text-color sys.color.on-error-container #3a211f
--tanrei-comp-snackbar-error-title-color comp.snackbar.error.supporting-text.color #3a211f
--tanrei-comp-snackbar-focus-ring-color sys.color.focus-ring #0070ac
--tanrei-comp-snackbar-focused-opacity — 1
--tanrei-comp-snackbar-hovered-opacity — 1
--tanrei-comp-snackbar-icon-gap sys.space.space150 12px
--tanrei-comp-snackbar-icon-opacity — 0.85
--tanrei-comp-snackbar-icon-size sys.icon.medium 24px
--tanrei-comp-snackbar-info-container-color sys.color.info-container #edf0ff
--tanrei-comp-snackbar-info-focus-ring-color sys.color.on-info-container #212839
--tanrei-comp-snackbar-info-icon-color sys.color.info #3066cb
--tanrei-comp-snackbar-info-outline-color sys.color.info #3066cb
--tanrei-comp-snackbar-info-supporting-text-color sys.color.on-info-container #212839
--tanrei-comp-snackbar-info-title-color comp.snackbar.info.supporting-text.color #212839
--tanrei-comp-snackbar-inverse-error-container-color sys.color.on-error-container #361d1b
--tanrei-comp-snackbar-inverse-error-focus-ring-color comp.snackbar.inverse-error.supporting-text.color #ffe1df
--tanrei-comp-snackbar-inverse-error-icon-color comp.snackbar.inverse-error.supporting-text.color #ff928c
--tanrei-comp-snackbar-inverse-error-outline-color sys.color.error #ff928c
--tanrei-comp-snackbar-inverse-error-supporting-text-color sys.color.error-container #ffe1df
--tanrei-comp-snackbar-inverse-error-title-color comp.snackbar.inverse-error.supporting-text.color #ffe1df
--tanrei-comp-snackbar-inverse-info-container-color sys.color.on-info-container #1c2334
--tanrei-comp-snackbar-inverse-info-focus-ring-color comp.snackbar.inverse-info.supporting-text.color #e1e8ff
--tanrei-comp-snackbar-inverse-info-icon-color comp.snackbar.inverse-info.supporting-text.color #8eafff
--tanrei-comp-snackbar-inverse-info-outline-color sys.color.info #8eafff
--tanrei-comp-snackbar-inverse-info-supporting-text-color sys.color.info-container #e1e8ff
--tanrei-comp-snackbar-inverse-info-title-color comp.snackbar.inverse-info.supporting-text.color #e1e8ff
--tanrei-comp-snackbar-inverse-success-container-color sys.color.on-success-container #172718
--tanrei-comp-snackbar-inverse-success-focus-ring-color comp.snackbar.inverse-success.supporting-text.color #daeed6
--tanrei-comp-snackbar-inverse-success-icon-color comp.snackbar.inverse-success.supporting-text.color #62c572
--tanrei-comp-snackbar-inverse-success-outline-color sys.color.success #62c572
--tanrei-comp-snackbar-inverse-success-supporting-text-color sys.color.success-container #daeed6
--tanrei-comp-snackbar-inverse-success-title-color comp.snackbar.inverse-success.supporting-text.color #daeed6
--tanrei-comp-snackbar-inverse-warning-container-color sys.color.on-warning-container #32200c
--tanrei-comp-snackbar-inverse-warning-focus-ring-color comp.snackbar.inverse-warning.supporting-text.color #ffe3cb
--tanrei-comp-snackbar-inverse-warning-icon-color comp.snackbar.inverse-warning.supporting-text.color #f39e3a
--tanrei-comp-snackbar-inverse-warning-outline-color sys.color.warning #f39e3a
--tanrei-comp-snackbar-inverse-warning-supporting-text-color sys.color.warning-container #ffe3cb
--tanrei-comp-snackbar-inverse-warning-title-color comp.snackbar.inverse-warning.supporting-text.color #ffe3cb
--tanrei-comp-snackbar-inverse-container-color sys.color.inverse-surface #1f1f1f
--tanrei-comp-snackbar-inverse-focus-ring-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-snackbar-inverse-supporting-text-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-snackbar-inverse-title-color comp.snackbar.inverse.supporting-text.color #f1f1f1
--tanrei-comp-snackbar-motion-enter-duration sys.motion.role.appear.toast.enter 350ms
--tanrei-comp-snackbar-motion-enter-easing sys.motion.easing.enter cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-snackbar-motion-exit-duration sys.motion.role.appear.toast.exit 300ms
--tanrei-comp-snackbar-motion-exit-easing sys.motion.easing.exit cubic-bezier(0.3, 0, 1, 1)
--tanrei-comp-snackbar-motion-expand-duration sys.motion.role.resize.large 400ms
--tanrei-comp-snackbar-motion-expand-easing sys.motion.easing.spatial cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-snackbar-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-snackbar-motion-state-duration sys.motion.role.state.default 250ms
--tanrei-comp-snackbar-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-snackbar-outline-opacity — 0.4
--tanrei-comp-snackbar-outline-width sys.border.hairline 1px
--tanrei-comp-snackbar-pressed-opacity — 1
--tanrei-comp-snackbar-success-container-color sys.color.success-container #e2f6df
--tanrei-comp-snackbar-success-focus-ring-color sys.color.on-success-container #1b2c1c
--tanrei-comp-snackbar-success-icon-color sys.color.success #077b32
--tanrei-comp-snackbar-success-outline-color sys.color.success #077b32
--tanrei-comp-snackbar-success-supporting-text-color sys.color.on-success-container #1b2c1c
--tanrei-comp-snackbar-success-title-color comp.snackbar.success.supporting-text.color #1b2c1c
--tanrei-comp-snackbar-supporting-text-color sys.color.on-surface #1b1b1b
--tanrei-comp-snackbar-supporting-text-max-lines — 3
--tanrei-comp-snackbar-supporting-text-under-title-opacity — 0.8
--tanrei-comp-snackbar-timeout-long — 10000ms
--tanrei-comp-snackbar-timeout-short — 4000ms
--tanrei-comp-snackbar-title-color comp.snackbar.supporting-text.color #1b1b1b
--tanrei-comp-snackbar-title-gap sys.space.space50 4px
--tanrei-comp-snackbar-title-max-lines — 3
--tanrei-comp-snackbar-warning-container-color sys.color.warning-container #ffeee0
--tanrei-comp-snackbar-warning-focus-ring-color sys.color.on-warning-container #362410
--tanrei-comp-snackbar-warning-icon-color sys.color.warning #9b5c00
--tanrei-comp-snackbar-warning-outline-color sys.color.warning #9b5c00
--tanrei-comp-snackbar-warning-supporting-text-color sys.color.on-warning-container #362410
--tanrei-comp-snackbar-warning-title-color comp.snackbar.warning.supporting-text.color #362410
--tanrei-comp-snackbar-controls-label-typescale sys.typescale.label 500 14px/1.4286
--tanrei-comp-snackbar-supporting-text-typescale sys.typescale.body 400 14px/1.5714
--tanrei-comp-snackbar-title-typescale sys.typescale.heading-6 500 14px/1.4286

Fixtures

Canonical renderings. Every platform snapshots each fixture under the same id so the results can be compared.

Generated from specs/snackbar.spec.json, tokens/comp/snackbar.tokens.json, design/tokens/

Guidelines

When to use

Use a snackbar to confirm something the person just did, when it finished without a screen of its own: “Conversation archived”, “Link copied”, “Photo saved”. Offer at most one action that undoes it or follows up on it.

A snackbar reports; it does not ask. A choice between two actions, an error that blocks the person’s work, or anything they must acknowledge belongs in a dialog or next to what it is about. The error form is for a failure the person can come back to, such as “Message not sent” with Retry.

Snackbar or dialog

Both appear above the page, and they do opposite jobs.

Aspect Snackbar Dialog
What it does Reports something that has already happened. Asks for a decision, or for something to be acknowledged, before the work goes on.
Focus Never moves. The person keeps working. Moves into the dialog. The page behind it cannot be used.
How long it stays A few seconds, or until its action or its close button is used. Until the person answers.
Actions One at most, and the message is complete without it. The answers to its question.

If people can ignore the message and nothing goes wrong, it is a snackbar. If the app cannot go on without an answer, it is a dialog. A message that belongs to one part of the screen and has to stay visible, such as what is wrong with a field, goes in that place instead, for example as the field’s supporting text.

Parts

Part Set with Notes
Bar inverse A raised surface of the scheme by default, or the opposite scheme’s colors; see On a surface, or inverse.
Status glyph status and statusLabel Optional. A status form draws its glyph before the text; see Status forms.
Title title (headline on the web) Optional. A short heading above the message, in a heavier style of the message’s size. Without it the message stands alone.
Message message Required. Plain text, one or two short sentences, announced whole. With a title, it is the description, and is drawn a little quieter than the title.
Action action (the label) Optional: “Undo”, “View”, “Retry”. Drawn as a tonal button: a pill one quiet step off its bar, with its label in the bar’s text color. The snackbar draws it with its own tokens, not as a Button, as tall as the small Buttons beside it. It moves to its own row when the text would need more than two lines beside it, counting the title’s and the message’s lines together, or when its label would wrap.
Expand button expandLabel, expanded An icon-only small Button, drawn as the close button is. Drawn only while the title or the message is longer than three lines. expandLabel is its accessible name, such as “Show full message”.
Close button closable and closeLabel Optional. An icon-only small Button. For a message that stays until it is closed, or one people may want gone sooner. closeLabel is its accessible name, in your product’s language.

The action, the expand button and the close button sit beside the text’s first line, all three as tall as a small Button.

The expand and close buttons

The expand and close buttons are icon-only Buttons of the small size, with the icon set’s expand_more, expand_less and close glyphs. They are tonal, as every close button in Tanrei is, and they take their color from the bar: the glyph is the color of the bar’s text, on a circle of that color made translucent. So they read as controls on every form, a surface, a status’s tint and an inverse bar alike, and never bring a color the bar does not already have. Their hover, focus and press are a Button’s, laid over the same color, and their focus ring is the bar’s own focus color.

A product may draw them as a bare glyph instead. It does that once, for every snackbar, in its design system layer, by setting comp.snackbar.close-button.variant to the text variant; the Material and Cupertino presets do exactly that, to keep the bare glyph their originals have.

The action

The action is a tonal button that sits on its bar. Its container is the bar’s own color with a faint layer of the label’s color over it, so it is the same quiet step off the bar in every form: on a surface or inverse, with a status or without. The label is the color of the bar’s text.

It is tonal because it is usually an undo, and the person who just made the mistake has to see at once that there is a button. A bare text label is easy to miss on the bar.

Never make it a filled button. An action important enough to seem to want a filled button does not belong in a snackbar, which disappears and is reached last in the focus order. Put it behind the screen’s main action, in a bottom sheet, instead.

A product may give the action another look. It does that once, for every snackbar, in its design system layer, by giving the action’s color tokens and the opacity of its tonal layer other values; there is no per-snackbar variant. The Material and Cupertino presets do exactly that, to keep the text action their originals have.

Status forms

status Glyph Announced Use for
default None Politely Most messages: what the app just did.
info info Politely Something worth knowing that is not the result of the person’s action.
success check_circle Politely A task that finished well, where the person was waiting for it.
warning warning Assertively Something that may go wrong unless the person acts.
error error Assertively A failure the person can come back to, with an action that retries or explains.

Status forms: info, success, warning and error

A status form draws its bar as a quiet tint of the status, with a hairline edge in the status’s color around it and the status’s glyph, in that color, before the text. The edge takes no space. The default form has no edge of its own, only the one its elevation draws. A status form is never told by color alone: the glyph’s shape tells the forms apart, and statusLabel gives the status in words, in your product’s language (“Error”). The label is read before the title and the message. Warning and error interrupt what is being read; the other forms wait for it.

On a surface, or inverse

By default a snackbar is drawn on a raised surface of the scheme: the bright surface, lifted by its elevation, that dialogs also stand on, with the scheme’s text color. Drawn inverse it takes the opposite scheme’s colors, a dark bar in a light scheme, so that it stands apart from whatever it lands on.

Drawn inverse: the default form and the four status forms

inverse Drawn
Not set (null, the default) As the design system chooses. The default system chooses a surface; the m3-like preset chooses inverse.
true Inverse.
false On a surface, also in a system whose choice is inverse.

On the web the attribute says which of the three it is: left out it is not set, inverse is true, and inverse="false" is false. show() takes inverse as an option.

A status form drawn inverse is that status’s form from the other theme, part for part. In a light theme the inverse info bar is the info bar a dark theme draws, with that theme’s text, glyph, edge and action; in a dark theme it is the light theme’s. The same holds at each contrast level. So an inverse status form uses no color the product does not already draw elsewhere, and its glyph and edge are as vivid as they are on a surface.

Being drawn inverse changes colors and nothing else: the size, the corners, the elevation, the layout, the glyphs, the motion, the timing, the announcement and the keys stay the same. It is a look, not a meaning. Do not use it to say that a message is important; the status forms and statusLabel say that. Prefer to decide it once for the product, in its design system layer, so that every snackbar looks like the same kind of message.

Long text

While collapsed, the title and the message each show at most three lines, and end with an ellipsis where they are cut. An expand button then appears between the action and the close button. Activating it shows the whole text, emits toggle with the new expanded value (expandedchange on the web), keeps focus on the button and does not close the snackbar. The bar grows upward from its bottom edge, or downward from its top edge when it is placed at the top end, at once where motion is reduced. Its content is aligned to the top, so the glyph, the first line and the controls beside it keep their distance from the top edge while the bar grows or shrinks, and an action on its own row keeps its distance from the bottom edge. Your code may also set expanded, which emits nothing.

If the expanded text is still taller than the space the host has, the bar takes that height and its text scrolls inside it, while the glyph and the controls stay in place. Assistive technology always gets the whole text, whether it is cut or not.

Placement

placement Where the bar stands Use it for
bottom (default) At the bottom of the window, centred, a margin above the bottom edge. Most products, and every product on a phone.
top-end At the top of the window on the end side: the top right, or the top left in a right-to-left layout. In a compact window (narrower than 600 in the default system) it stands across the top instead. The desktop screens of a business application, where the bottom of the screen is where the work is.

Choose one placement for the whole product and give it to the host, so that people learn where messages appear: placement on <tanrei-snackbar-host> applies to every snackbar it shows, and a snackbar asked for with its own placement keeps it. On a phone keep the bottom, where Material puts the snackbar and where the thumb is. On the desktop screens of a business application, such as a dashboard, an admin console or an editor whose own toolbar, status bar or table pagination sits at the bottom, use top-end, where Carbon and Fluent put their notifications.

At the top end the bar stands a margin below the top edge and from the end edge, inside the safe area, and below a sticky header, so that it never covers the top app bar. On the web it reads the header’s height from --tanrei-app-shell-scroll-offset, which App shell sets on the document’s root; a product with a frame of its own sets that property to the height of its own sticky header. It comes down from above the top edge instead of rising from below, and an expanded bar grows downward. Nothing else changes: it is still one snackbar at a time and never a stack, its controls are still reached at the host’s place in the focus order, and it is announced the same way.

<!-- Every snackbar this host shows stands at the top end. -->
<tanrei-snackbar-host placement="top-end"></tanrei-snackbar-host>

<script type="module">
  const snackbars = document.querySelector('tanrei-snackbar-host');
  snackbars.show({ message: 'Report exported', action: 'Open' });
  // One snackbar asked for at the bottom keeps its own placement.
  snackbars.show({ message: 'Photo saved', placement: 'bottom' });
</script>
<SnackbarHost ref={snackbars} placement="top-end" />

SwiftUI, Compose, Flutter and React Native do not have the top-end placement yet: their snackbars stand at the bottom.

Duration

duration How long it stays by itself
auto (default) short without an action. With an action it never times out, so nobody is timed out of it (WCAG 2.2.1).
short Closes after the short timeout (4 seconds in the default system).
long Closes after the long timeout (10 seconds in the default system).
indefinite Stays until the person or your code closes it.

A timed snackbar pauses while the pointer is over it, while focus is inside it, while it is expanded, while the app is hidden or in the background, and while a Tanrei modal dialog covers it. It then starts again from its whole length. Android’s accessibility timeout lengthens it when that is longer, and a snackbar with an action never times out while a screen reader is running. If you time out a snackbar that has an action, also offer that action elsewhere, such as Undo in a menu. Debug builds remind you.

Showing, focus and closing

A snackbar is a read overlay. People take in its message, so showing it never moves focus.

  • One at a time. The host shows snackbars in the order they were requested. The next one waits until the last one has faded out. A snackbar requested while a Tanrei modal dialog is open waits until the dialog closes.
  • Placement. Bottom centre, a margin above the bottom edge and from the sides, inside the safe area and above the on-screen keyboard; or, with placement="top-end", at the top end, below a sticky header (see Placement). Either way it is drawn above its area’s content and below a modal dialog and its scrim.
  • Announcement. When it appears, assistive technology reads the status label, the title and the message once, followed by the action’s label: politely, after what is being read, or assertively for warning and error. Nothing takes keyboard or screen-reader focus.
  • Focus. The action, the expand button and the close button are reached with Tab at the host’s place in the focus order, which is why the host goes last. The bar itself is not a Tab stop, except its text while an expanded bar scrolls it.
  • Closing. The snackbar closes when its action is activated (action), or without it (dismiss), for one of these reasons:
Reason When
close The close button was activated.
escape Escape was pressed while focus was inside the snackbar.
timeout A short or long snackbar ran out of time.
host Your code closed it or cleared the queue.
  • Focus back. When a snackbar closes while focus is inside it, focus returns to the element that had it before focus entered the snackbar.
  • Motion. It rises into place from below the bottom edge while fading in, or comes down from above the top edge at the top end, and fades out in place. Where motion is reduced it fades without moving; on Android, whose setting removes every animation, it appears and disappears at once.
  • Say what happened in the past tense: “Message sent”, “3 files moved”.
  • Give the action a short verb that people expect after the message: “Undo”, “View”, “Retry”.
  • Choose the status that matches what happened, and give every status form a statusLabel.
  • Decide once, for the whole product, whether snackbars are drawn on a surface or inverse, and where they stand.
  • Keep the text short enough that it rarely needs the expand button.
  • Keep the default auto duration unless you have a reason. It never times anyone out of an action.
  • Put the host last on the page or in the layout, so its controls come last in the focus order.

Don’t

  • Don’t use a snackbar for errors that block the task or for questions. Use a dialog.
  • Don’t put two actions in a snackbar. It has room for one.
  • Don’t make the action a filled button, or put an action that seems to need one in a snackbar. Use a bottom sheet from the main action.
  • Don’t use the warning or error form for routine confirmations. They interrupt the screen reader.
  • Don’t draw a snackbar inverse to mark it as important. Inverse changes colors only; a status says what kind of message it is.
  • Don’t show a snackbar for something the screen already shows clearly.
  • Don’t move focus to a snackbar or its action. It must not take people away from their work.

Accessibility

Role
status
Labeling
The snackbar is a live region: polite in the default, info and success forms (the status role on the web) and assertive in the warning and error forms (the alert role on the web). When it appears it announces, once, statusLabel, the title, the whole message and the action's label. The status glyph is hidden from assistive technology; statusLabel gives the status in words. The action's name is its label; the close button's name is closeLabel; the expand button's name is expandLabel, and it exposes whether the text is expanded. The bar itself is not a tab stop; its text is one only while it scrolls.
Minimum target
The hit area meets the minimum target size even when the visual size is smaller. minimum 32px · pointer 44px · spacing 8px · touch 48px

Keyboard

Keys Action
Tab Reaches, at the host's place in the focus order, the text while it scrolls, then the action, the expand button and the close button. Showing a snackbar never moves focus.
Enter / Space Activates the focused action, expand button or close button.
Arrow keys, Page Up, Page Down While the expanded text scrolls and has focus, scroll it.
Escape While focus is inside the snackbar, closes it (dismiss, reason escape).

Notes

  • WCAG 2.2.1 (timing adjustable): by default (auto) a snackbar with an action never times out. A product may time one out (short or long), but then the action must also be available elsewhere (Undo in a menu or a history), because a person who is slow, zoomed-in or using a screen reader may not reach it in time. The timeout pauses while the snackbar is hovered, focused or expanded, and Android's accessibility timeout lengthens it.
  • A snackbar reports; it does not ask. A choice between two actions, an error that blocks the person's work, or anything they must acknowledge belongs in a dialog or next to what it is about. The error form is for a failure the person can come back to (a message not sent, with Retry).
  • The action is a tonal button so that an undo is seen at once: its label is the strongest thing in it (at least 9.1:1 on the action at rest and 6.9:1 in its weakest state, in every form and scheme of the default system), and the action is one quiet step off its bar, the same step in every form (1.25–1.45:1 against the bar in all ten forms and six schemes), so it never outweighs the message and never weighs more on one status than on another. A button with a visible label needs no boundary contrast (WCAG 1.4.11). An action important enough to seem to want a filled button does not belong in a snackbar: put a Main Action Button in a bottom sheet instead.
  • WCAG 2.4.13 and 1.4.11 (focus appearance, non-text contrast): the focus indicator is drawn on the bar, so its color is the form's (SNK-3). On the default form's surface it is the system's focus color, which the build holds against every surface role; on a status bar and on an inverse bar it is that bar's content color, which the build holds against the bar at 4.5:1 or more. The system's focus color alone is held against no status bar and falls under 3:1 on the inverse surface in five of the six schemes.
  • Drawn inverse or not, a snackbar is the same message: the role, the announcement, the focus order and the timing do not change with its colors (SNK-30). A product should not use inverse to mean something (an inverse bar for important messages): that is what the status forms and statusLabel are for.
  • WCAG 1.4.1 (use of color): a status form is never told by color alone. It draws the status's glyph, a shape of the icon set, and gives statusLabel, the status in words, to assistive technology (SNK-21).
  • WCAG 1.4.3 and 1.4.11 (contrast): three parts are drawn at an opacity of their own color, so their contrast is that of the color as drawn, not of the role. In the default system the message under a title keeps at least 7.1:1 on its bar (7.6:1 in the high-contrast schemes) and the status glyph at least 3.6:1, in every form and scheme (SNK-32, SNK-21); a status form's edge is decoration beside the glyph and statusLabel and is held to nothing (SNK-31). A system that changes one of these opacities, or the colors under them, must keep 4.5:1 for the message and 3:1 for the glyph.
  • WCAG 1.4.11 (non-text contrast): the expand and close buttons are Buttons in the form's tone (SNK-34): the glyph is the bar's message color, and the circle under it is that color at comp.button.tone.container.opacity over the bar. In the default system the glyph keeps at least 8.0:1 against its circle at rest and 6.2:1 in its weakest states (focused and pressed), in all ten forms and six schemes — well above the 3:1 a glyph without a label needs. A system that changes the tone's opacity, the state-layer opacities or a form's colors must keep 3:1 for the glyph against the circle in every state.
  • Text size never hides text for good: a collapsed title or message ends with an ellipsis at its line limit and the expand button shows the rest (SNK-23); the action moves to its own row (SNK-16); an expanded bar taller than the window scrolls inside it (SNK-25).
  • Web: `<tanrei-snackbar-host>` is placed by the product, normally as the last element of the page (its place in the focus order). It shows `<tanrei-snackbar>` elements as `popover="manual"`, so they are in the top layer, and exposes `show(options): Promise<{ result: 'action' | 'dismiss', reason? }>` (whose options include `inverse`), `dismissCurrent()` and `clear()`. The live regions are the host's two, which are always in the document, so screen readers register them before the first message is inserted: one with the status role (polite) for the default, info and success forms, one with the alert role (assertive) for warning and error. The expand button carries `aria-expanded` and `aria-controls` naming the text.
  • SwiftUI, Compose, Flutter and React Native announce with the platform's own call, at default priority for the polite forms and at high priority for warning and error: AccessibilityNotification.Announcement with the speech announcement priority, a polite or assertive liveRegion, SemanticsService.announce with its assertiveness, and AccessibilityInfo.announceForAccessibility (announceForAccessibilityWithOptions with queue off for assertive on iOS). None of them takes focus. The expand button's state is the platform's expanded/collapsed semantics (Compose's expand and collapse actions, Flutter's expanded flag, React Native's accessibilityState.expanded, and on Apple platforms the expanded and collapsed value VoiceOver reads for a disclosure).
  • How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

Generated from specs/snackbar.spec.json, tokens/sys/measure.tokens.json

What the component handles

  • The snackbar is announced through a live region: a polite one (the status role on the web) for the default, info and success forms, and an assertive one (the alert role) for warning and error. Its status label, title and message, then its action’s label, are announced once when it appears, and always whole, even when the text is cut. On SwiftUI, Compose, Flutter and React Native the platform’s own announcement is used, at high priority for warning and error, and none of them takes focus.
  • A status form is never told by color alone: it draws the status’s glyph and exposes statusLabel as text before the title. The glyph itself is hidden from assistive technology, and so are the expand and close glyphs.
  • The action is named by its label, the expand button by expandLabel and the close button by closeLabel. All are reached with Tab at the host’s place in the focus order, and activated with Enter or Space.
  • The expand button exposes whether the text is expanded, and keeps the same name in both states. An expanded bar whose text scrolls makes the text a focus stop, named by the title and scrolled with the arrow keys and Page Up and Page Down.
  • Escape closes the snackbar while focus is inside it. When it closes with focus inside, focus returns to where it was before.
  • Where the bar stands changes nothing else. A snackbar at the top end is reached with Tab at the host’s place in the focus order and announced exactly as one at the bottom, and it never covers a sticky header.
  • A snackbar with an action does not time out by default. Timed snackbars pause while hovered, focused or expanded, lengthen to Android’s accessibility timeout and, with an action, never time out while a screen reader runs.
  • The action meets the minimum target on both axes without changing its drawn size, and the expand and close buttons, which are Buttons, keep their own. Every focus ring shows for keyboard focus only, in a color of the snackbar’s form, so that it is visible on every bar: a surface, a status’s tint and an inverse bar.
  • The expand and close glyphs keep at least 3:1 against their circle in every state and form. In the default system they keep at least 8.0:1 at rest, and 6.2:1 focused or pressed.
  • The parts that are drawn softer keep their contrast against the bar in every scheme of the default system: the status glyph at least 3:1, and a message under a title at least 4.5:1, or 7:1 in the high-contrast schemes. The softening is a matter of color only; what assistive technology reads does not change, and neither does it when the snackbar is drawn inverse.
  • Larger text never hides text for good: a collapsed title or message ends with an ellipsis at three lines and the expand button shows the rest, the action moves to its own row, and an expanded bar taller than the window scrolls.
  • Before the icon font has loaded, no glyph’s name is ever drawn as text: the glyph’s place stays empty until the font can draw it.
  • Debug builds report an empty message, a close button without closeLabel, a status form without statusLabel, an expand button without expandLabel, and a timed snackbar that has an action.

Known platform gaps:

  • Web: the browser does not say whether a screen reader is running and has no accessibility timeout, so neither lengthens a timed snackbar. The default auto duration is what keeps an action reachable.
  • Web: keeping clear of the on-screen keyboard has not yet been checked on a device that has one.
  • SwiftUI cannot return focus to the element that had it: when a snackbar closes with focus inside, focus goes where SwiftUI moves it for a removed view. A host inside a view you host outside a scene, such as your own NSHostingView, never runs its timeouts.
  • Compose cannot name the element that had focus before it entered the snackbar. When the snackbar closes around the focus, focus moves back out the way it came in: after tabbing in, to the element before the host. When your code closes it while focus is inside, focus does not move.
  • Compose: lengthening to Android’s accessibility timeout and pausing while the window is hidden are not yet covered by a test.
  • Compose: the polite announcement, the pause while the window has no focus and the accessibility timeout have been tested on Robolectric only, not yet on a device.
  • Flutter does not yet lengthen a timed snackbar to Android’s accessibility timeout, which Flutter does not expose.
  • React Native cannot tell which element had focus before. When a snackbar closes while focus is inside it, focus goes where the platform puts it.
  • React Native has no safe-area API of its own. The snackbar keeps inside the safe area only when you pass the window’s insets to TanreiProvider; without them, on a phone with a home indicator it may sit partly over it.
  • React Native decides that the action moves to its own row from the laid-out lines, so the move happens after the first layout pass.
  • React Native: the polite announcement, keeping above the keyboard, and the pauses for the background and the pointer have not yet been checked on a device.

What you need to do

  • Write the message as plain text that makes sense on its own when read aloud.
  • Give the close button a closeLabel, a status form a statusLabel, and a snackbar whose text may run long an expandLabel, all in your product’s language.
  • Place the host last on the page, or last in the layout on Compose.
  • On React Native, pass the window’s insets to TanreiProvider, so the snackbar keeps clear of the system bars.
  • If you choose short or long for a snackbar with an action, offer the same action somewhere that does not time out.
  • Put anything people must act on in a dialog, not a snackbar.