> ## Oymyakon DS index
> Fetch the full index at: https://super-dollop-pzmo65r.pages.github.io/llms.txt
> HTML preview: https://super-dollop-pzmo65r.pages.github.io/snackbar.html
> Source: specs/components/snackbar/snackbar.md, specs/components/snackbar/snackbar-a11y.md, specs/components/snackbar/capabilities.json

---

# Snackbar · Oymyakon DS 3

> The actionable toast: a pill that slides in from the bottom of the screen with one short
> message, an optional icon and an optional action button, and dismisses itself after 4 seconds.

**Version:** 3.0.0 · **Status:** Draft · **Figma:** `[Snackbar] 3.0` (node `7445:13212`)

---

## 1. Description

Snackbar is the transient message that can carry an action. It slides in from the bottom of the
screen over the current content, delivers one short line ("Message deleted", "No connection"),
optionally offers a single small button ("Undo", "Retry"), and leaves on its own after 4 seconds.
The user can dismiss it earlier with a downward swipe. Only one Snackbar is on screen at a time: a
new message replaces the current one — the visible pill exits, the new one enters. Both platforms
ship it: Android `DsSnackbar`, iOS `DsSnackbar` (presented via `DsSnackbarAlert`).

It ships in two tones — **Inform** (inverse surface) and **Custom** (the instance supplies a
semantic colour pair) — selected by the `Style` variant, and the action button docks in one of two positions selected by
`Button position`: **Aside** (at the row's end) or **Below** (under the text). The component also
has an **RTL** axis (`RTL: Off / On`) that mirrors the horizontal order for right-to-left locales.

Snackbar and [Notification](https://super-dollop-pzmo65r.pages.github.io/notification.md) are the two DS toasts and share one
behaviour contract — the same timing, the same gestures, the same replace-never-stack rule. They
differ by edge and by role: Notification falls from the top and carries the statuses that need to
be seen — **including every error**; Snackbar rises from the bottom, informs and can act. That is
why Snackbar has no error tone (owner, 2026-09-04): a failure is a Notification, not a bottom
toast. Unlike **HeaderAlert**, both are ephemeral and self-dismissing.

> Platform naming note (recorded in `manifest/bindings/android.json`): Android maps Figma's
> `Inform` to `DsSnackbar.Style.Neutral`; iOS keeps `.inform`. The DS name is Inform.

## 2. Anatomy

```
┌─ Snackbar (full width, s16 side margins) ─────────────────────┐
│  ┌─ ContentContainer (pill, radius s20, padding s8) ────────┐ │
│  │                                                          │ │
│  │   [IconContainer s24]  Description   [ActionButton]      │ │  ← Aside
│  │                        [ActionButton]                    │ │  ← Below
│  └──────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
```

| Element | Required | Description |
|---|---|---|
| **ContentContainer** | Required | The visible pill: tone fill, radius `s20`, `s8` padding all round. Fixed heights — `s56`, or 108 with the button below (§ 8). |
| **IconContainer** | Optional | Leading icon slot — an [`IconContainer`](https://github.com/inDriver/oymyakon-ds/blob/main/specs/primitives/icon-container.md) with a `s24` glyph, vertically centred. The default glyph is `warning-outline` for both styles; the author may substitute another DS icon or hide the slot entirely (`Show Icon: false`). |
| **Description** | Required | One short message, Compact Body. Up to 2 lines at 100 % scale, then ellipsis; unclamped in the 130 % / 150 % accessibility scale modes (§ 7). |
| **ActionButton** | Optional | A [`customButton`](https://super-dollop-pzmo65r.pages.github.io/button.md) instance — the open base, preset `custom-button`: height `s40`, side padding `s12`, radius `s10` (instance-set — the base's `s20` would read as a capsule at this height), a single Heading 4 title row (default label "Text"), fill `Background/Primary`, label `TextAndIcon/Primary`. Off by default (`Button: false`); its dock is the `Button position` axis — and the dock is adaptive: a message that stops fitting beside the button moves it below (§ 5). |

The component owns its side margins: the root spans the full screen width and insets the pill by
`s16` on each side.

### RTL layout

With `RTL: On` the horizontal order mirrors: the IconContainer moves to the inline end of the
reading direction, the Description aligns to the right, and the button mirrors with its dock —
Aside moves to the far side of the row, Below keeps its indent under the text from the right. The
default glyph (`warning-outline`) is non-directional and does not flip (per `icon-container.md`
§ 3.5).

## 3. Variants and sizes

| Parameter | Values | Default |
|---|---|---|
| `Style` | `Inform` / `Custom` | `Inform` |
| `Button position` | `Aside` (auto — falls back to Below) / `Below` (forced) | `Aside` |
| `Button` | `true` / `false` | `false` |
| `Show Icon` | `true` / `false` | `true` |
| `RTL` | `Off` / `On` | `Off` |

One width: the pill is full width minus the `s16` side margins. Two fixed heights (§ 8): `s56`
everywhere except `Button position: Below` with the button on, which is 108. Per-style tokens:

| Style | Fill | Content colour | Default icon |
|---|---|---|---|
| Inform | `Background/InversePrimary` | `TextAndIcon/InversePrimary` | `outlined/actions/warning-outline` |
| Custom | instance-set semantic token | instance-set semantic token | `outlined/actions/warning-outline` |

Custom is a tint, not a semantic tone: the instance supplies both colours as semantic tokens
(never raw values) and owns their contrast — the preview's green example is illustrative only (it
measures 3.83 / 2.78 against the 4.5:1 body-text target, like every accent + AlwaysLight pair).
It carries no meaning of its own — a failure is never
a Custom Snackbar but a Negative [Notification](https://super-dollop-pzmo65r.pages.github.io/notification.md).

> **Figma drift (owner, 2026-09-04):** the live set still carries `Style: Inform | Negative`; the
> Negative member is removed from the DS contract in favour of Notification, and the Figma-side
> update (rename to Custom / drop Negative) is the owner's manual follow-up.

The action button keeps one look on both styles: fill `Background/Primary`, label
`TextAndIcon/Primary` — deliberately counter-phase to the Inform pill, so when the dark theme
turns the inverse pill light, the button turns dark and the pair never merges. This pair is
snackbar-owned: it matches none of the six Button styles (§ 6).

## 4. States

The first / rest state is named **Standard**. The snackbar body is a status container; its only
interactive element is the optional action button, whose press feedback belongs to the Button
component.

| State | Description |
|---|---|
| Standard | Pill visible at rest — tone fill, icon + description, the button when on. |
| Entering | Pill slides up from off-screen past the bottom edge (see § 5). |
| Exiting | Pill slides back down off the bottom edge — after the 4 s timeout, on swipe-down, on the action, or when a new Snackbar replaces it. |

There is no pressed, disabled, or loading state of the snackbar itself; the nested button carries
its own `pushButton` press (§ 5).

## 5. Animation and behavior

Reference: [`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md). No hardcoded easing or durations.

Lifecycle — the Notification contract, mirrored to the bottom edge: the pill enters from the
bottom, holds for **4 seconds** (platform default), and exits back down. A downward swipe
dismisses it immediately; tapping the action fires it and dismisses. When a new Snackbar arrives
while one is visible, the visible pill plays its exit and the new one enters — the two never stack.

**Adaptive dock (owner, 2026-09-04).** `Aside` is a preference, not a promise: the text keeps up
to 2 lines beside the button, and a message that would truncate even so moves the button below —
the component measures and relocates on its own, the author is never asked. `Below` remains
available as the forced dock. The relocation is a layout decision made before the pill enters —
the dock never changes mid-flight, so nothing animates between the two compositions. (The platform
APIs currently take the position explicitly — the bridge notes record `ButtonPosition.End/.Below`
— so where the runtime does not yet measure, the integrator applies the same rule.)

| Event | Pattern | Token | CSS |
|---|---|---|---|
| Appear | `Transforming/Enter/Default` | `--transforming-enter-default` | `transform: translateY(100% → 0)` (250 ms) |
| Hide (timeout / swipe / action / replace) | `Transforming/Exit/Default` | `--transforming-exit-default` | `transform: translateY(0 → 100%)` (200 ms) |
| Button press | `pushButton` | `--component-push-button-press` / `-release` | the nested Button's own scale 100 % → 95 % |

Only the compositor-friendly `transform` property is animated — never layout, colour, or height.
The timing matches Notification and HeaderAlert — one entrance for the family of edge banners.

```css
@media (prefers-reduced-motion: reduce) {
  /* --transforming-enter-default / -exit-default and --component-push-* resolve
     to 0ms at the token — no component override needed */
}
```

## 6. Color tokens

One token name serves both themes — Light/Dark substitution happens at the token.

| Element | Token (Light) | Token (Dark) |
|---|---|---|
| Fill — Inform | `Background/InversePrimary` | `Background/InversePrimary` |
| Fill — Custom | instance-set semantic token | the same token's Dark value |
| Text + icon — Inform | `TextAndIcon/InversePrimary` | `TextAndIcon/InversePrimary` |
| Text + icon — Custom | instance-set semantic token | the same token's Dark value |
| Action button — fill | `Background/Primary` | `Background/Primary` |
| Action button — label | `TextAndIcon/Primary` | `TextAndIcon/Primary` |

In CSS: `var(--background-inverse-primary)`, `var(--text-and-icon-inverse-primary)`,
`var(--background-primary)`, `var(--text-and-icon-primary)`; a Custom instance passes its pair
through the private vars `--snackbar-fill` / `--snackbar-content` — semantic tokens only.

The button pair is a snackbar-owned configuration of the open customButton base — none of the six
Button styles carries `Background/Primary` + `TextAndIcon/Primary` (Button § 6). On Inform the
pair is exactly counter-phase to the pill in both themes.

## 7. Typography

| Element | Style |
|---|---|
| Description | Compact Body (Suisse Intl Book, 14/16) |
| Action button label | Heading 4 (Suisse Intl Semibold, 17/20) — the customButton base default, not the `S` preset's Main Body |

Line behaviour — the Notification contract:

- **100 % scale** — up to **2 lines**, longer text truncates with an ellipsis. The pill's height
  does not change: two lines centre within the fixed text row. With the button aside the same
  2-line allowance applies to the narrower column — but before truncating, the component moves the
  button below (§ 5), and only a message that overflows 2 lines at FULL width earns the ellipsis.
- **130 % / 150 % accessibility scale modes** — the clamp is removed, and the height tokens scale
  with the mode, so a third line still fits and the message is never cut for a large-type user.

The button label holds a single line (Button § 7).

## 8. Spacing

| Property | Token |
|---|---|
| Side margins (root → pill) | `s16` per side |
| Pill padding (all four sides) | `s8` |
| Icon slot inner leading padding | `s8` (Aside composition) |
| Gap glyph → text | `s12` visual (composed from the container pads) |
| Gap text → button (Aside) | `s12` visual |
| Gap content row → button row (Below) | `s8` |
| Button indent under the text (Below) | 44 from the content edge — the `s32` icon box + the `s4` gap + the slot's own `s8` inset, so the button starts exactly where the text does |
| Content row height | `s40` |
| Action button | `s40` tall, `s12` side padding, radius `s10` (instance-set; the `S` preset's own radius differs) |
| Pill height — everywhere except Below + button | `s56` (fixed) |
| Pill height — Below with the button on | 108 (fixed): 8 + 40 + 8 + 40 + 12 |
| Corner radius | `s20` |
| Icon glyph size | `s24` |

Both heights are fixed — a second text line centres within the `s40` content row instead of
growing the pill, the same mechanics as Notification — and both height tokens scale with the SP
modes (`s56` is 56 → 72.8 → 84 at 130/150 %), so a large-type client gets the extra lines the way
Notification does. With the icon hidden the text column starts at the pill padding; nothing else
moves. In the Below + button composition the bottom air is 12 (the `s8` pill padding plus the
button row's own 4) — the one asymmetry the composition carries.

The icon box carries its own `s8` inline-start inset for a reason: the pill padding is `s8` —
sized so the button sits 8 from the edge — while the glyph keeps the toast family's 16 from the
edge. The slot pays the difference (8 + 8), not the pill; the description's own `s8` insets do the
same for the text, composing the visual 12s with the `s4` container gaps.

## 9. Usage context

**Use it for** a transient message that offers one small recovery or follow-up: an undoable
outcome ("Message deleted" + Undo), a retryable failure ("No connection" + Retry). Without the
button it is interchangeable with [Notification](https://super-dollop-pzmo65r.pages.github.io/notification.md) — prefer
Notification for pure statuses at the top, Snackbar when the message needs an action or belongs
to the bottom of the flow.

**Do not use it for**:

- Errors. Every failure surfaces as a Negative [Notification](https://super-dollop-pzmo65r.pages.github.io/notification.md) at
  the top of the screen, where it is seen — the bottom toast has no error tone by design.
- Persistent or blocking states that wait for the user — that is
  [HeaderAlert](https://super-dollop-pzmo65r.pages.github.io/header-alert.md).
- More than one action. One button; a decision with alternatives belongs in a dialog.
- Long copy: the pill clamps at 2 lines at 100 % scale.

Related components: [Notification](https://super-dollop-pzmo65r.pages.github.io/notification.md) (the top-edge, non-actionable
toast), [HeaderAlert](https://super-dollop-pzmo65r.pages.github.io/header-alert.md) (persistent top banner),
[Button](https://super-dollop-pzmo65r.pages.github.io/button.md) (the nested action).

## 10. Accessibility

The pill announces itself when it appears — always politely (`role="status"`): with errors living
in Notification, no Snackbar message is critical enough to interrupt current speech. The icon is decorative and unlabeled. The action button is a real focusable
button — the one tab stop the component has. The entry announcement carries the message text alone;
the action is discovered by swipe navigation after it — a message whose action must be known
immediately says so in its own words ("Message deleted — undo available"). Swipe-down has a
non-gesture equivalent on both platforms, and the 4-second timeout extends while a screen reader
is active — with an action present the timing weighs even heavier, and one open gap is recorded:
keyboard and motor-impaired users without a screen reader get no extension mechanism.

Touch target is a known shortfall: the action borrows Button `S` geometry at `s40` — below the DS
`s44` floor and the 48 WCAG 2.5.5 recommends — and Button's dense-context rationale for `S` does
not obviously apply to a floating pill. Like the contrast shortfall, resolution is the owner's
call before Ready.

Contrast: the Inform pairing is the inverse pair (17:1 / high) and the action button pair
(`Background/Primary` + `TextAndIcon/Primary`) is theme-native — both pass; a Custom instance
supplies its own semantic pair and owns its contrast check.

Full spec: [`snackbar-a11y.md`](https://super-dollop-pzmo65r.pages.github.io/snackbar.md).

## 11. Analytics and coverage contract

Reference: `docs/prototype-analytics-and-coverage.md` (repo root).

| Field | Value |
|---|---|
| `data-ds-component` | `snackbar` |
| Coverage unit | yes |
| Tap target model | multiple targets (the action button; the root takes the swipe) |
| Actions | `tap` (on the action button) · `dismiss` (swipe on the root) |
| Internal targets | `action` |
| Emits value | no |

The pill root is the coverage unit and the swipe target; the action button is the single tap
target. Auto-dismiss by timeout is not a user action and emits nothing.

Required prototype markup:

```html
<div
  class="snackbar"
  data-ds-component="snackbar"
  data-ds-component-id="{screen}.{module}.{instance}"
  data-ds-variant="inform"
  data-ds-state="standard"
  data-ds-action="dismiss">
  <div class="snackbar__content">
    <div class="snackbar__row" role="status">
      <span class="snackbar__icon" aria-hidden="true"><!-- IconContainer, s24 glyph --></span>
      <span class="snackbar__description">Snackbar message</span>
    </div>
    <button type="button" class="button snackbar__button"
            data-ds-component="button" data-ds-preset="custom-button"
            data-ds-target="action" data-ds-action="tap">
      <span class="button__title-row">Text</span>
    </button>
  </div>
</div>
```

---

## Changelog

Newest entry first.

| Version | Date | Change |
|---|---|---|
| 3.0.0 | 2026-09-04 | First version: Style (Inform / Custom) × Button position (Aside / Below) × RTL (the Figma set still carries Negative — removed from the contract in favour of Notification, Figma follow-up pending), booleans Button (off) and Show Icon (on); the bottom-edge actionable toast on the Notification behaviour contract (4 s auto-dismiss, swipe-down, replace-never-stack, Transforming Enter/Exit Default); open-customButton action (s40, Heading 4, `Background/Primary` + `TextAndIcon/Primary`); fixed heights `s56` / 108. |

---

# Snackbar — Accessibility

The actionable toast: everything the Notification contract establishes — announce on entry,
never steal focus, dismiss without the gesture, extend the timeout under a screen reader — plus
one interactive element, the action button, which must be reachable before the toast leaves.

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Snackbar (root) | Description text, verbatim | — | The message row (icon + description) is the `polite` live region (`accessibilityLiveRegion`); the pill root and the button sit outside it, so the action is never re-announced. With errors living in Notification, no Snackbar interrupts current speech | — |
| IconContainer | — | — | Not important for accessibility (decorative, `importantForAccessibility="no"`) | — |
| Description | Read as part of the root announcement, not focusable separately | — | — | — |
| ActionButton | Button label ("Text" by default) | — | Button | Discovered by swipe navigation after the announcement — the announcement itself carries the message text only |

- The appearance fires the live-region announcement once; the pill does not steal accessibility
  focus. It stays reachable by swipe navigation and touch exploration while visible — the button
  is the component's one focusable element.
- Dismiss: the root exposes the standard `ACTION_DISMISS` accessibility action so TalkBack users
  dismiss from the pill's actions menu instead of the two-finger swipe.
- Timeout: while TalkBack is active the 4 s auto-dismiss extends until the announcement has
  finished — and long enough for the user to reach and press the action. A toast whose action
  cannot be reached in time is a timing failure, not a shorter message. Open gap: the extension is
  screen-reader-scoped, while WCAG 2.2.1 also covers keyboard and motor-impaired users without one.

### Edge states

- **No icon (`Show Icon: false`):** the announcement is unchanged — the icon is decorative and
  never part of the accessible name.
- **No button (`Button: false`):** the component has no focusable elements and behaves exactly as
  Notification does.
- **Replaced by a new Snackbar:** the new message fires its own announcement; the outgoing pill
  leaves silently.
- **Focus on the action when the pill leaves — on any exit path** (replace, timeout, swipe): focus
  returns to the user's previous position rather than being dropped with the removed node.
- **Reduced motion:** the pill appears and leaves without the slide; announcement behaviour is
  unchanged.
- **RTL:** mirrored layout only — reading order and announcement are unchanged.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Snackbar (root) | Description text, verbatim | — | Announced via `UIAccessibility.Notification.announcement` on entry; the pill itself is a static text element | — |
| IconContainer | — | — | Excluded (decorative, `isAccessibilityElement = false`) | — |
| Description | Read as part of the root announcement, not focusable separately | — | — | — |
| ActionButton | Button label ("Text" by default) | — | Button | Discovered by swipe navigation after the announcement — the announcement itself carries the message text only |

- Presenting through `DsSnackbarAlert` posts the announcement; VoiceOver focus stays where the
  user is. The action button is reachable by swipe navigation while the toast is visible.
- Dismiss: the root implements `accessibilityPerformEscape()` (two-finger Z scrub) as the
  non-gesture equivalent of swipe-down.
- Timeout: the 4 s auto-dismiss extends while VoiceOver is running — until the announcement
  completes and the action, when present, can be reached. The same open gap as on Android: no
  extension exists for users without a screen reader.

### Edge states

- **Dimmed:** not applicable — the snackbar has no disabled state; the nested Button's own
  disabled form is not part of this composition.
- **No icon / no button:** announcement unchanged; without the button there are no focusable
  elements.
- **Focus on the action when the pill leaves — on any exit path** (replace, timeout, swipe): focus
  returns to the user's previous position rather than being dropped with the removed node.
- **RTL:** mirrored layout only — reading order and announcement are unchanged.

---

## Web preview (reference)

The preview page and prototypes mark the pill up as a live region so the same contract holds:

- Always `role="status"` (implicit `aria-live="polite"`) — both styles, and it sits on the
  MESSAGE ROW (icon + description), never on the pill root; errors are Notification's job and
  arrive with its `role="alert"` at the top of the screen.
- The live-region container stays mounted (empty) for the lifetime of the screen; each message
  mutates its text content. Inserting a fully-populated live region in one DOM mutation is not
  reliably announced (Safari/VoiceOver and some NVDA builds skip it) — only text changes inside an
  existing region announce everywhere.
- The action is a real `<button>` inside the pill but OUTSIDE the live region — a sibling of the
  message row, which is why the region is the row and not the root: AT/browser combinations
  differ on whether a focusable descendant's name is read as part of the region announcement
  (double-speak) or suppressed. The mutated message text alone drives the announcement; the
  button is focusable with Tab, activated with Space/Enter per the Button contract — the
  component's only tab stop.
- The icon's SVG carries `aria-hidden="true"`; the accessible name is the description text.
- **Custom is a tint, never a meaning:** the instance-set colour pair carries no semantics — a
  colour-impaired user loses nothing by not distinguishing Custom from Inform. Anything that MUST
  be distinguished (an error) is a Negative Notification, not a Snackbar.
- **Touch target — known shortfall (owner's call):** the action is `s40` tall (Button `S`
  geometry) — below the DS `s44` floor (typography rules) and the 48 of WCAG 2.5.5; Button's
  dense-context rationale for `S` does not obviously apply to a floating pill.
- **Contrast:** the Inform pairing is the inverse pair (17:1 / high) and the action button
  (`TextAndIcon/Primary` on `Background/Primary`) is the theme's native text-on-surface pair —
  both pass. A Custom instance supplies its own semantic pair and OWNS its contrast check
  (≥ 4.5:1 for the 14 px text) before shipping.

---

## Machine contract — `specs/components/snackbar/capabilities.json`

Axes, allowed values, defaults, constraints and CSS/Figma bindings. Read this instead of guessing what the component can do.

```json
{
  "$schema": "../component-capabilities.schema.json",
  "id": "snackbar",
  "name": "Snackbar",
  "version": "3.0.0",
  "description": "The actionable toast: a pill that slides in from the BOTTOM of the screen with one short message, an optional icon and an optional action button, and dismisses itself after 4 seconds — or earlier, on a downward swipe or the action. One on screen at a time; a new message replaces the current one. No error tone by design — every failure is a Negative Notification at the top (owner, 2026-09-04). Figma: [Snackbar] 3.0 — component set 7445:13212 (componentKey 5bba9bfee0a52bac576c39ec5b45490b24ba0bb7); the live set still carries Style=Inform|Negative — Negative is removed from the DS contract, the Figma-side rename to Custom is the owner's manual follow-up.",
  "files": {
    "spec": "specs/components/snackbar/snackbar.md",
    "a11y": "specs/components/snackbar/snackbar-a11y.md",
    "preview": "src/snackbar.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "Oymyakon 3.30.1 — components"
  },
  "root": {
    "class": "snackbar",
    "dataDsComponent": "snackbar"
  },
  "anatomy": {
    "content": {
      "class": "snackbar__content",
      "notes": "The visible pill (Figma ContentContainer): tone fill, radius var(--sp-s20), padding var(--sp-s8) all round, s4 container gaps. FIXED heights — var(--sp-s56), or 108 with the button docked below (8 + 40 + 8 + 40 + 12); a 2-line message centres within the s40 content row instead of growing the pill, and the height tokens scale with the SP modes. The root spans the full width and owns the var(--sp-s16) side margins."
    },
    "row": {
      "class": "snackbar__row",
      "notes": "The icon + text row (Figma icon+text) — structural: fills the width the button leaves, s40 tall, s4 gap."
    },
    "icon": {
      "class": "snackbar__icon",
      "optional": true,
      "notes": "Leading icon slot — an IconContainer: var(--sp-s32) x var(--sp-s40) box with a var(--sp-s8) inline-start inset, var(--sp-s24) glyph painted via currentColor. The inset exists because the pill padding is sized for the button — 8 + 8 returns the glyph to the toast family's 16 from the edge. Default glyph warning-outline for both styles; Show Icon off removes the node. Decorative — never part of the accessible name."
    },
    "description": {
      "class": "snackbar__description",
      "notes": "One short message, Compact Body, with its own var(--sp-s8) side insets (they compose the visual 12s with the s4 gaps). Clamped to 2 lines with an ellipsis at 100 % scale; the 130/150 % modes lift the clamp and the height tokens scale with them."
    },
    "button": {
      "class": "snackbar__button",
      "optional": true,
      "notes": "The action — the OPEN customButton base (preset custom-button), configured by the snackbar: var(--sp-s40) tall, var(--sp-s12) side padding, radius var(--sp-s10) (instance-set — the base's s20 would read as a capsule at this height), Heading 4 title (the base default, not the S preset's Main Body), width hugs the label. Snackbar-owned pair Background/Primary + TextAndIcon/Primary — counter-phase to the inverse pill, matching none of the six Button styles. Press is the Button's own pushButton."
    }
  },
  "axes": {
    "style": {
      "title": "Style",
      "type": "enum",
      "values": [
        "inform",
        "custom"
      ],
      "default": "inform",
      "customizable": "The tone. Inform is the base class (fill var(--background-inverse-primary), content var(--text-and-icon-inverse-primary)); snackbar--custom takes an instance-set semantic pair through the private vars --snackbar-fill / --snackbar-content — semantic tokens only, never raw values, and the instance owns the ≥ 4.5:1 contrast check. Custom is a tint, never a meaning: there is NO error tone — a failure is a Negative Notification. Binds to the Figma variant property Style (whose Negative member is retired; rename pending)."
    },
    "buttonPosition": {
      "title": "Button position",
      "type": "enum",
      "values": [
        "aside",
        "below"
      ],
      "default": "aside",
      "customizable": "The action's dock — snackbar--below is the Below composition (button aligned with the text start: margin-inline-start s32 + s4 + s8 = 44 from the content edge). ADAPTIVE: aside is a preference — the component itself measures the message in the Aside dock (rendered text height against two line-heights) and relocates the button below when it stops fitting; below remains available as the forced dock. The dock is decided before the pill enters and never changes mid-flight. Binds to the Figma variant property Button position (Aside → ButtonPosition.End, Below → .Below on the platforms)."
    },
    "button": {
      "title": "Button",
      "type": "boolean",
      "default": false,
      "customizable": "Presence of the action. No modifier class — the node is either in the markup or it is not (Android button: DsSnackbar.Button?, iOS omits the button(_:) call). Without it the component has no focusable elements and behaves as Notification does."
    },
    "showIcon": {
      "title": "Show icon",
      "type": "boolean",
      "default": true,
      "customizable": "Presence of the leading IconContainer. No modifier class — the node is either in the markup or it is not (Android iconResId: null, iOS omits the icon(_:) call)."
    },
    "actionLabel": {
      "title": "Action label",
      "type": "text",
      "default": "Text",
      "customizable": "The button's single Heading 4 line — one short verb (\"Undo\", \"Retry\"). It is the button's accessible name."
    },
    "descriptionText": {
      "title": "Description",
      "type": "text",
      "default": "Snackbar message",
      "customizable": "One short message. It is the announcement, verbatim — a message whose action must be known immediately says so in its own words."
    }
  },
  "states": {
    "default": [
      "standard",
      "entering",
      "exiting"
    ]
  },
  "constraints": [
    "One width: full minus the root's own var(--sp-s16) side margins. Two FIXED heights — var(--sp-s56), or 108 with the button below (annotation arithmetic 8 + 40 + 8 + 40 + 12); text never grows the pill, the height tokens scale with the SP modes instead.",
    "No error tone. Every failure surfaces as a Negative Notification at the top of the screen; Custom is a tint (instance-set semantic pair via --snackbar-fill / --snackbar-content), never a meaning.",
    "The adaptive dock: Aside is a preference — the component measures the message beside the button and relocates it below on overflow, on its own; the author is never asked. The dock is decided before entry and never animates mid-flight.",
    "Transient by contract: auto-dismiss after 4 seconds (the platform default); a downward swipe or the action dismisses earlier. A message the user must resolve belongs to HeaderAlert.",
    "One Snackbar on screen at a time. A new message plays the visible pill's exit and enters after it — never a stack, never a queue.",
    "No pressed, disabled, or loading state of the snackbar itself; the nested Button carries its own pushButton press and its disabled form is not part of this composition.",
    "Announced as a polite live region (role=\"status\") — both styles, no assertive form. The announcement carries the message text alone; the action is discovered by navigation and is the component's only tab stop. On web the region stays mounted, only its text mutates, and the button lives outside the aria-live text node.",
    "The icon is decorative and silent: importantForAccessibility=\"no\" / isAccessibilityElement = false / aria-hidden=\"true\".",
    "Only transform animates: enter var(--transforming-enter-default) (250 ms), exit var(--transforming-exit-default) (200 ms) — translateY past the BOTTOM edge, the Notification/HeaderAlert timing. Layout, colour and height never animate; reduced motion is zeroed at the token.",
    "Touch target — known shortfall (owner's call before Ready): the action is var(--sp-s40) tall, below the DS var(--sp-s44) floor and the 48 of WCAG 2.5.5."
  ],
  "analytics": {
    "dataDsComponent": "snackbar",
    "action": "tap",
    "notes": "Multiple targets: the pill root is the coverage unit and the swipe target (data-ds-action=\"dismiss\"); the action button is the single tap target (data-ds-target=\"action\", data-ds-action=\"tap\", carrying its own data-ds-component=\"button\" data-ds-preset=\"custom-button\"). Auto-dismiss by timeout is not a user action and emits nothing. data-ds-variant carries the tone (inform | custom); there is no preset axis."
  },
  "rtl": {
    "supported": true,
    "notes": "The rows mirror from the document's dir — the flex order reverses on its own; the icon's inline-start inset and the Below indent (margin-inline-start) are logical, so they flip with it. Nothing flips in place: warning-outline is not on the icon-container.md § 3.5 directional list. In Figma the mirrored composition is a variant of the set (RTL: Off | On)."
  }
}
```
