# ASSnackbar

`ASSnackbar` presents one temporary, non-blocking notification. Its owner controls visibility and any ordering or queue. The only interaction is optional dismissal; messages must contain non-interactive content. Use a separate banner for persistent information or acknowledgment.

```tsx
<ASSnackbar
  open={open}
  message={<><strong>Saved.</strong> Your project is updated.</>}
  variant="success"
  duration={5000}
  dismissLabel="Dismiss"
  onClose={() => setOpen(false)}
  onClosed={showNextNotification}
/>
```

`duration` is positive finite milliseconds, default **5000**, with a maximum of 2147483647 (the browser timer limit). There is no indefinite sentinel. This is the normal display lifetime: entry grace can extend it and manual dismissal can end it earlier. Duration is captured at opening. Changing message content, callback identity, or duration during that occurrence does not start a new lifetime. Close then reopen in separate renders, or use a new React `key` to replace a notification while keeping `open` true. Remounting starts a new occurrence; it does not emit the previous instance's `onClosed`. The Storybook Show notification button increments a React key on every press, so triggering again while visible gets the full configured duration. Merely setting an already-true `open` value to true does not restart it.

`onClose(reason)` requests controlled closure once per occurrence. Reasons are `timeout`, `dismiss`, and `escape`. The parent must set `open=false`. The panel remains mounted while sliding down below the viewport, then `onClosed()` runs after removal. Start the next queued notification after this callback, without guessing animation durations. Unmounting or reopening during exit does not emit `onClosed` for that interrupted exit. No IDs, queue, Redux, or persistence live here.

The display countdown starts after the slide-in completes. Entry and exit use `components.snackbar.motionDuration` and `motionEasing`, derived from the shared motion tokens (0.25s and ease-in-out by default). The hook waits for the actual surface animation; it does not maintain a second animation timer. Reduced-motion preference skips animation and completes the lifecycle immediately. During exit the surface is inert. Reopening cancels pending exit cleanup and starts a new visible lifetime.

Pointer entry grants `Math.max(remaining, 2000)` milliseconds. The countdown continues while hovered; pointer exit changes nothing. Re-entry grants the same minimum. Touch entry does not grant hover grace. Focus entry also grants at least two seconds without pausing. On removal, focus returns to the previously focused element only if focus remained inside and that element is still available. Moving focus elsewhere prevents restoration. Escape dismisses only from within the surface. Timeout and manual dismissal races produce one request.

The message is announced in a persistent polite status region; dismissal is outside that region. Appearance never focuses the snackbar. Decorative message icons should use `aria-hidden`. Actual assistive-technology announcement behavior still requires screen-reader testing; DOM assertions are not that proof.

Hover or focus during slide-in is recorded too: the display deadline is the later of the normal duration after entry and two seconds after the interaction. Closing or unmounting clears pending grace so it cannot carry into another occurrence.

`dismissLabel` may be Okay, Close, Dismiss, or another localized dismissal label. The control uses ASButton's ASTappable pointer path inside a single focusable dismissal button wrapper. The visual child is hidden from accessibility so its text decoration and pointer pressed state do not turn dismissal into an announced toggle. The wrapper owns keyboard activation and naming. There is no arbitrary action callback. `ref` targets the surface div while open.

`size` accepts `ComponentSize.Small` (`ASSmall`), `ComponentSize.Medium` (`ASMedium`, the default), or `ComponentSize.Large` (`ASLarge`). It sets message typography, surface padding/gap, and the dismissal button size together. Theme overrides for typography and spacing live under `components.snackbar.sizes[size]`. The surface has an independent `8px` default radius so wrapped content does not inherit a pill-shaped button outline. On narrow screens the message wraps, and the dismissal control can move to a second row aligned to the end.

The surface is fixed at bottom center with `width: max-content`, wrapping, a maximum width, and safe-area gutters. Short messages occupy only the space they need; content changes resize the panel through CSS without measurement state. Long messages remain constrained by the viewport and themed maximum width. Like ASModal, the component portals to `document.body`, so narrow, clipping, or transformed application wrappers do not constrain its positioning or width. React theme context is retained across the portal; CSS variables used by a theme must be available at the portal destination. In an iframe, the viewport and body belong to that iframe.

Storybook keeps configuration in Controls and uses a centered, themed ASButton to show each notification. The Default example inherits the global Yellow / Blue toolbar selection and shared demo font; see [Storybook themes](./storybook-themes.md). All examples under ASUI Components/ASSnackbar wait for the reader to interact; none run scripted clicks or keyboard input. Keyboard, expiry, repeated-trigger, and content-update examples show labeled completion and dismissal results. Automated sequences live separately under ASUI Checks/ASSnackbar, with explicitly named Automated stories. Demo state, styling, and automated checks live in `src/stories/assnackbar`; public story metadata lives in `ASSnackbar.stories.tsx`.

`theme` replaces the inherited complete theme; `themeOverrides` composes with it. `components.snackbar` defines typography, geometry and info/success/error colors. Descendant dismissal buttons receive the composed theme. Classic preserves the yellow library fallback; neutral supplies the blue Storybook example.
