> ## 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/notification.html
> Source: specs/components/notification/notification.md, specs/components/notification/notification-a11y.md, specs/components/notification/capabilities.json

---

# Notification · Oymyakon DS 3

> A transient toast pill that slides in from the top of the screen, shows one short status
> message with an optional leading icon, and dismisses itself after 4 seconds or on swipe.

**Version:** 3.1.4 · **Status:** Draft · **Figma:** `[Notification] 3.0` (node `7331:2650`)

---

## 1. Description

Notification is a transient, non-blocking status message — the DS toast. It slides in from the
top of the screen over the current content, delivers one short line of feedback ("Saved",
"No connection", "Payment failed"), and leaves on its own after 4 seconds. The user can dismiss
it earlier with an upward swipe. Only one Notification 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 `DsNotification`, iOS `DsNotificationView` (presented via `DsNotificationAlert`).

It ships in three tones — **Neutral** (inverse surface), **Positive** (`Accent/Green 2`) and
**Negative** (`Accent/Red 2`) — selected by the `Style` variant. Structure and typography are
identical across tones; only the fill, the content colour token, and the default icon change.
The component also has an **RTL** axis (`RTL: Off / On`) that mirrors the horizontal order for
right-to-left locales.

Unlike **HeaderAlert** (a persistent page-level banner anchored to the top chrome until the state
resolves), Notification is ephemeral and self-dismissing. Unlike **[Snackbar](https://super-dollop-pzmo65r.pages.github.io/snackbar.md)**, it never
carries an action button — it only informs.

## 2. Anatomy

```
┌─ Notification (full width, s8 side margins) ──────────────────┐
│  ┌─ ContentContainer (pill, radius s20, fixed height s56) ──┐ │
│  │                                                          │ │
│  │   [IconContainer s24]   Description (Compact Body)       │ │
│  │                                                          │ │
│  └──────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
```

| Element | Required | Description |
|---|---|---|
| **ContentContainer** | Required | The visible pill: tone fill, radius `s20`, fixed `s56` height — a second text line centres within it, the pill does not grow. |
| **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. Each Style provides a default glyph (§ 3); the author may substitute another DS icon or hide the slot entirely (`Show Icon: false` on the platforms). |
| **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). |

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

### RTL layout

With `RTL: On` the horizontal order mirrors: the IconContainer moves to the inline end of the
reading direction (visually the right edge in RTL) and the Description aligns to the right. The
default glyphs (`warning-outline`, `done-outline`, `warning-tr-outline`) are non-directional and
do not flip (per `icon-container.md` § 3.5).

## 3. Variants and sizes

| Parameter | Values | Default |
|---|---|---|
| `Style` | `Neutral` / `Positive` / `Negative` | `Neutral` |
| `RTL` | `Off` / `On` | `Off` |
| `Show Icon` | `true` / `false` | `true` |

One size: the pill is full width (minus the `s8` side margins) and a fixed `s56` tall — one or
two text lines centre within the same pill. Per-style defaults:

| Style | Fill | Content colour | Default icon |
|---|---|---|---|
| Neutral | `Background/InversePrimary` | `TextAndIcon/InversePrimary` | `outlined/actions/warning-outline` |
| Positive | `Accent/Green 2` | `TextAndIcon/AlwaysLight` | `outlined/actions/done-outline` |
| Negative | `Accent/Red 2` | `TextAndIcon/AlwaysLight` | `outlined/actions/warning-tr-outline` |

## 4. States

The first / rest state is named **Standard**. Notification is a status container with no
interactive elements — dismissal is a gesture on the whole pill, and there is no press feedback.

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

There is no pressed, disabled, or loading state.

## 5. Animation and behavior

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

Lifecycle: the pill enters from the top, holds for **4 seconds** (platform default), and exits
back up. An upward swipe dismisses it immediately. When a new Notification arrives while one is
visible, the visible pill plays its exit and the new one enters — the two never stack.

| Event | Pattern | Token | CSS |
|---|---|---|---|
| Appear | `Transforming/Enter/Default` | `--transforming-enter-default` | `transform: translateY(-100% → 0)` (250 ms) |
| Hide (timeout / swipe / replace) | `Transforming/Exit/Default` | `--transforming-exit-default` | `transform: translateY(0 → -100%)` (200 ms) |

Only the compositor-friendly `transform` property is animated — never layout, colour, or height.
The timing matches HeaderAlert, the other top-edge banner — one entrance for the family. (3.1.0
documented `moveM`; the owner retimed it on 2026-09-03 — 400 ms read as slow for a toast.)

```css
@media (prefers-reduced-motion: reduce) {
  /* --transforming-enter-default / -exit-default resolve to 0ms at the token —
     the pill appears and leaves instantly; 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 — Neutral | `Background/InversePrimary` | `Background/InversePrimary` |
| Fill — Positive | `Accent/Green 2` | `Accent/Green 2` |
| Fill — Negative | `Accent/Red 2` | `Accent/Red 2` |
| Text + icon — Neutral | `TextAndIcon/InversePrimary` | `TextAndIcon/InversePrimary` |
| Text + icon — Positive / Negative | `TextAndIcon/AlwaysLight` | `TextAndIcon/AlwaysLight` |

In CSS: `var(--background-inverse-primary)`, `var(--accent-green2)`, `var(--accent-red2)`,
`var(--text-and-icon-inverse-primary)`, `var(--text-and-icon-always-light)`.

> Platform note (recorded in `manifest/bindings/android.json`): the Android DS3 source carries a
> known quirk — `Style.Positive` reads the red accent value and `Style.Negative` the green one.
> Figma is the source of truth: Positive is green, Negative is red.

## 7. Typography

| Element | Style |
|---|---|
| Description | Compact Body (Suisse Intl Book, 14/16) |

Line behaviour:

- **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 `s56`, the visual air compressing from 16
  to 12 (centring arithmetic — the declared padding stays `s16`).
- **130 % / 150 % accessibility scale modes** — the clamp is removed, and the `s56` height token
  itself scales with the mode, so a third line still fits and the message is never cut for a
  large-type user.

Compact Body is a body style and scales **full** per the typography scaling rules, and the fixed
`s56` height token scales with the same modes — 56 → 72.8 → 84 per the generated SP tokens —
which is what makes the room for the third line.

## 8. Spacing

| Property | Token |
|---|---|
| Side margins (root → pill) | `s8` per side |
| Pill padding (all four sides) | `s16` |
| Gap icon → text | `s12` |
| Pill height (fixed) | `s56` |
| Corner radius | `s20` |
| Icon glyph size | `s24` |

The `s24` icon + `s16` padding above and below = the `s56` height; the 16-tall Compact Body line
centres against the icon (the 20 of air above the text is centring arithmetic, not a padding).
The height is fixed: it holds when the icon is hidden, and a second text line centres within the
same `s56` — the air above and below compresses to 12 instead of the pill growing.

## 9. Usage context

**Use it for** transient, self-resolving feedback: an action's outcome ("Order sent",
"Address saved"), a recoverable failure ("No connection — retrying"), a background status change.
The message does not depend on being seen — the pill leaves after 4 seconds regardless.

**Do not use it for**:

- Persistent or blocking states that wait for the user to act — that is [HeaderAlert](https://super-dollop-pzmo65r.pages.github.io/header-alert.md),
  which stays until the state resolves.
- Messages carrying an action ("Undo", "Retry" as a button) — that is [Snackbar](https://super-dollop-pzmo65r.pages.github.io/snackbar.md).
- Long copy: the pill clamps at 2 lines at 100 % scale. If the message doesn't fit, it belongs in
  a different surface.

Related components: [HeaderAlert](https://super-dollop-pzmo65r.pages.github.io/header-alert.md) (persistent top banner),
[Snackbar](https://super-dollop-pzmo65r.pages.github.io/snackbar.md) (the bottom-edge actionable toast).

## 10. Accessibility

The pill announces itself when it appears: Neutral and Positive messages politely
(`role="status"`), Negative assertively (`role="alert"`). The icon is decorative and unlabeled —
the announcement is the description text. Swipe-up has a non-gesture equivalent on both
platforms (accessibility dismiss action / escape). The 4-second timeout extends while a screen
reader is active so the announcement is never cut short.

Contrast is a known shortfall on the accent fills: white 14 px text measures Positive 3.83 / 2.78
and Negative 3.98 / 3.47 (Light / Dark) against the 4.5:1 body-text target — a token-level pairing
shared with HeaderAlert; the owner resolves it before the component flips to Ready.

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

## 11. Analytics and coverage contract

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

| Field | Value |
|---|---|
| `data-ds-component` | `notification` |
| Coverage unit | yes |
| Tap target model | root (swipe-to-dismiss gesture) |
| Actions | `dismiss` |
| Internal targets | — |
| Emits value | no |

The pill root is both the coverage unit and the single gesture target; it reports `dismiss` when
the user swipes it away. Auto-dismiss by timeout is not a user action and emits nothing.

Required prototype markup:

```html
<div
  class="notification notification--neutral"
  data-ds-component="notification"
  data-ds-component-id="{screen}.{module}.{instance}"
  data-ds-variant="neutral"
  data-ds-state="standard"
  data-ds-action="dismiss"
  role="status">
  <div class="notification__content">
    <span class="notification__icon"><!-- IconContainer, s24 glyph --></span>
    <span class="notification__description">Notification message</span>
  </div>
</div>
```

---

## Changelog

Newest entry first.

| Version | Date | Change |
|---|---|---|
| 3.1.4 | 2026-09-04 | Snackbar shipped its spec — the three "(planned)" references now point at the real component; no contract change. |
| 3.1.3 | 2026-09-03 | Review fixes: the §7 cap130 claim corrected — the `s56` height token scales full with the modes (56 → 72.8 → 84 per the generated SP tokens), matching the shipped CSS and the third-line guarantee; §10 now surfaces the accent-fill contrast shortfall (the HeaderAlert §10 pattern); the lifecycle demo instance carries `data-ds-action="dismiss"`. |
| 3.1.2 | 2026-09-03 | Height contract corrected to Figma: the pill is a fixed `s56` — a 2-line message centres within it (air 16 → 12) instead of growing the container; in the 130/150 % modes the height token itself scales, making room for the third line. `min-height` → `height` in the web build. |
| 3.1.1 | 2026-09-03 | Enter/exit retimed (owner): `moveM` (400 ms) → `Transforming/Enter/Default` (250 ms) + `Transforming/Exit/Default` (200 ms) — the HeaderAlert timing; 400 ms read as slow for a toast. `transform`-only and token-level reduced motion unchanged. |
| 3.1.0 | 2026-09-03 | First version: Style (Neutral / Positive / Negative) × RTL, hideable IconContainer slot, 2-line clamp at 100 % / unclamped at 130–150 %, moveM enter/exit from the top, 4 s auto-dismiss, swipe-up dismiss, replace-not-stack. Versioned 3.1.0 on the owner's call (2026-09-03); the Figma set frame still carries the `3.0` line name. |

---

# Notification — Accessibility

Transient toast: the core accessibility problem is that the message appears without user action
and leaves on its own. The component announces itself on entry, offers a non-gesture dismissal,
and never times out mid-announcement.

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Notification (root) | Description text, verbatim | — | Live region: `polite` for Neutral / Positive, `assertive` for Negative (`accessibilityLiveRegion`) | — |
| IconContainer | — | — | Not important for accessibility (decorative, `importantForAccessibility="no"`) | — |
| Description | Read as part of the root announcement, not focusable separately | — | — | — |

- The appearance fires the live-region announcement once; the pill does not steal accessibility
  focus from the user's current position. It stays reachable by swipe navigation and touch
  exploration while visible — reachability is what makes its actions menu discoverable.
- 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 never shorter than the platform's minimum for transient messages).

### Edge states

- **No icon (`Show Icon: false`):** the announcement is unchanged — the icon is decorative and
  never part of the accessible name.
- **Negative:** announced assertively (interrupts current speech); Neutral / Positive queue
  politely behind current speech.
- **Replaced by a new Notification:** the new message fires its own announcement; the outgoing
  pill leaves silently.
- **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 |
|---|---|---|---|---|
| Notification (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 | — | — | — |

- Presenting through `DsNotificationAlert` posts the announcement; VoiceOver focus stays where
  the user is.
- Dismiss: the root implements `accessibilityPerformEscape()` (two-finger Z scrub) as the
  non-gesture equivalent of swipe-up.
- Timeout: the 4 s auto-dismiss extends while VoiceOver is running so the announcement completes.

### Edge states

- **Dimmed:** not applicable — the component has no disabled state.
- **No icon:** announcement unchanged.
- **Negative:** may interrupt in-progress speech; Neutral / Positive wait for the current phrase.
- **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:

- Neutral / Positive: `role="status"` (implicit `aria-live="polite"`).
- Negative: `role="alert"` (implicit `aria-live="assertive"`).
- 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 icon's SVG carries `aria-hidden="true"`; the accessible name is the description text.
- **Contrast — known shortfall (measured 2026-09-03, WCAG 2.x relative luminance):** white 14 px
  Book text on the accent fills does not reach the 4.5:1 body-text target in either theme —
  Positive `Accent/Green 2` 3.83:1 Light / 2.78:1 Dark, Negative `Accent/Red 2` 3.98:1 Light /
  3.47:1 Dark (Neutral is fine at 17:1 / high). These are the palette's shipped accent pairings,
  shared with HeaderAlert (its a11y file carries the same open note). Token-level issue for the
  colour owner; blocking resolution is the owner's call before the component flips to Ready.

---

## Machine contract — `specs/components/notification/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": "notification",
  "name": "Notification",
  "version": "3.1.4",
  "description": "The transient toast: a pill that slides in from the top of the screen, delivers one short status message with an optional leading icon, and dismisses itself after 4 seconds — or earlier, on an upward swipe. One on screen at a time; a new message replaces the current one. Figma: [Notification] 3.0 — component set 7331:2650 (componentKey 005218eae176801ed603922926afcbe37a624ce1), variants Style=Neutral|Positive|Negative x RTL=Off|On. Versioned 3.1.0 on the owner's call (2026-09-03); the Figma set frame still carries the 3.0 line name.",
  "files": {
    "spec": "specs/components/notification/notification.md",
    "a11y": "specs/components/notification/notification-a11y.md",
    "preview": "src/notification.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "Oymyakon 3.30.1 — components"
  },
  "root": {
    "class": "notification",
    "dataDsComponent": "notification"
  },
  "anatomy": {
    "content": {
      "class": "notification__content",
      "notes": "The visible pill (Figma ContentContainer): tone fill, radius var(--sp-s20), FIXED height var(--sp-s56), padding var(--sp-s16) all round, gap var(--sp-s12). The pill does not grow: a 2-line message centres within it (visual air 16 -> 12); in the 130/150 % scale modes the height token itself scales. The root spans the full width and owns the var(--sp-s8) side margins."
    },
    "icon": {
      "class": "notification__icon",
      "optional": true,
      "notes": "Leading icon slot — an IconContainer: invisible var(--sp-s24) box, glyph painted via currentColor. Each Style brings its default glyph (warning-outline / done-outline / warning-tr-outline); Show Icon off removes the node entirely. Decorative on every platform — never part of the accessible name."
    },
    "description": {
      "class": "notification__description",
      "notes": "One short message, Compact Body. Clamped to 2 lines with an ellipsis at 100 % scale; the 130/150 % accessibility modes lift the clamp — the scaled height token makes room for the third line."
    }
  },
  "axes": {
    "style": {
      "title": "Style",
      "type": "enum",
      "values": [
        "neutral",
        "positive",
        "negative"
      ],
      "default": "neutral",
      "customizable": "The tone. Neutral is the base class (fill var(--background-inverse-primary), content var(--text-and-icon-inverse-primary)); notification--positive (var(--accent-green2)) and notification--negative (var(--accent-red2)) pin the content to var(--text-and-icon-always-light). Only the fill, the content colour and the default glyph change — structure and typography are identical. Binds to the Figma variant property Style."
    },
    "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). The fixed var(--sp-s56) height holds when the icon is gone."
    },
    "iconGlyph": {
      "title": "Icon glyph",
      "type": "enum",
      "values": [
        "style-default"
      ],
      "default": "style-default",
      "customizable": "Any DS icon may be swapped into the var(--sp-s24) box. The per-style defaults are outlined/actions/warning-outline (Neutral), done-outline (Positive), warning-tr-outline (Negative) — none is on the icon-container.md § 3.5 directional list, so the default glyph never mirrors; a directional replacement opts into the RTL mirror at the instance per that section."
    },
    "descriptionText": {
      "title": "Description",
      "type": "text",
      "default": "Notification message",
      "customizable": "One short outcome that survives being missed. It is the whole accessible name — the announcement is this string, verbatim."
    }
  },
  "states": {
    "default": [
      "standard",
      "entering",
      "exiting"
    ]
  },
  "constraints": [
    "One size: the pill is full width minus the root's own var(--sp-s8) side margins at a fixed var(--sp-s56) height — one or two clamped lines centre within the same pill. There is no size ladder.",
    "No pressed, disabled, or loading state. Nothing on the pill is tappable — the only interaction is the whole-pill upward swipe, which dismisses immediately.",
    "Transient by contract: auto-dismiss after 4 seconds (the platform default). A message the user must resolve or act on belongs to HeaderAlert or Snackbar, not here.",
    "One Notification on screen at a time. A new message plays the visible pill's exit and enters after it — never a stack, never a queue.",
    "The description clamps at 2 lines with an ellipsis at 100 % scale; the 130/150 % accessibility scale modes lift the clamp and scale the height token with it, so the message is never cut for a large-type user.",
    "The icon is decorative and silent: importantForAccessibility=\"no\" / isAccessibilityElement = false / aria-hidden=\"true\". The accessible announcement is the description text alone.",
    "Announced as a live region on appearance — polite for Neutral / Positive, assertive for Negative — without stealing accessibility focus. On web the region stays mounted and only its text mutates.",
    "Only transform animates: enter var(--transforming-enter-default) (250 ms), exit var(--transforming-exit-default) (200 ms) — the HeaderAlert timing — translateY past the top edge. Layout, colour and height never animate; reduced motion is zeroed at the token, never overridden here.",
    "Colour follows Figma: Positive is var(--accent-green2), Negative is var(--accent-red2). The Android DS3 source carries a known name-swap quirk, recorded in manifest/bindings/android.json — Figma is the source of truth."
  ],
  "analytics": {
    "dataDsComponent": "notification",
    "action": "dismiss",
    "notes": "The pill root is both the coverage unit and the single gesture target; it reports dismiss when the user swipes it away. Auto-dismiss by timeout is not a user action and emits nothing. data-ds-variant carries the tone (neutral | positive | negative); there is no preset axis."
  },
  "rtl": {
    "supported": true,
    "notes": "The row mirrors from the document's dir — the flex order reverses on its own and the geometry is symmetric, so no class is needed. Nothing flips in place: the three default glyphs are not on the icon-container.md § 3.5 directional list. In Figma the mirrored composition is a variant of the set (RTL: Off | On)."
  }
}
```
