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

---

# FloatingContainer · Oymyakon DS 3

> An elevated rounded card that floats over the screen's content and holds DS components — a call
> to action, a pair of buttons, a short group of controls — while the content underneath stays in
> view.

**Version:** 3.1.1 · **Status:** Ready · **Figma:** `[FloatingContainer] 3.1` (main components file `7vdl5YkZFDWvh9QvSmydsH`, page `15691:39482`; the public master `[FloatingContainer] 3.1` `15691:40935` (key `b5572c1d…`) — a single component, no variant set: the `s8` wrapper with the booleans `Overlay` and `Home Indicator`, hosting the card `FloatingContainer` `36135:19436` (key `fac3d322…`, one `slot`, in the section Building blocks `15691:46590`), the gradient `.background-overlay` (set `12398:3239`) and `.Home Indicator iOS` (set `15053:10526`), both from the `[TabBar] 3.1` page; the Specification frame `15691:41819`; the brief "Update 3.1" `36840:11574`, ZCP-3120)

---

## 1. Description

FloatingContainer is a card that floats above the content of a screen and carries other DS
components. The Specification frame: "a container for floating elements on the screen, intended to
hold design-system components. It is used where a Floating Button cannot be applied." Teams place
in it:

- primary buttons (CTA);
- secondary actions;
- small groups of controls;
- contextual information.

It is a container, not a control: it has no state, no action and no text of its own — everything
inside it is the host's components, each with its own contract. The container contributes the
elevation (a card with a border and a shadow), the margins around it, and two pieces of edge chrome
taken from the screen's bottom: a **gradient overlay** that fades the content scrolling under the
card, and the **home-indicator area** below it.

Two styles (the frame: "The component has a custom variant and one preset built on the custom
variant"):

- **L-size** — the preset, the default: `s4` padding, `s24` corners, `Surface/Floating`,
  `Border/Transparent`, the M shadow — locked.
- **CustomFloatingContainer** — the base the preset is built on: the same numbers, each a default a
  design request may re-point (§ 3).

### Platform implementations

| Platform | Entry point | Version | Notes |
|---|---|---|---|
| iOS | `DsFloatingContainer` (DS3) | — | `init(@ViewBuilder content:)` — a generic container, the content closure is its only input. It delegates to `DsCustomFloatingContainer`, which carries `layout(DsCustomFloatingContainerLayout)` and `appearance(DsCustomFloatingContainerAppearance)` — the type for a tuned overlay or background. The bridge states no Figma version |
| Android | `DsFloatingContainer` (`core.compose.ds.v3.container`) | 3.0 | `style = DsFloatingContainer.Style` (only `L` exists); `content: @Composable () -> Unit` required. `DsCustomFloatingContainer` (same file) takes a custom radius, padding, border and shadow. The bridge names the Figma component `[FloatingContainer] 3.0` |
| Flutter | — | — | Not shipped |
| Web | `src/shared/shared.css` (`.floating-container`) | 3.1.0 | Reference implementation, this repo; preview page `src/floating-container.njk` |

Recorded platform divergences:

- **Overlay and Home Indicator are not parameters** on either platform — both bridges record the two
  Figma booleans as "decoration" and "padding" with no argument. The web exposes them as two
  modifiers (§ 3), on by default as in Figma.
- **The 3.1 overlay colour.** Android's bridge still names `[FloatingContainer] 3.0`; whether its
  overlay follows the 3.1 change (`Background/BaseWhite` → `Background/Primary`, § 6) is not
  recorded. In Night the difference shows: `Background/BaseWhite` stays white over a dark screen.

---

## 2. Anatomy

```
[FloatingContainer] (.floating-container)      the public master · fills the host's width · s8 around the card
├── Overlay (::before)                         optional, on by default · Background/Primary, transparent → opaque, behind the card
├── Card (.floating-container__card)           Surface/Floating · Border/Transparent 1px outside · shadow M · radius s24
│   └── Content (the slot)                     the host's components · s4 from the card's edge · min-height s24
└── Home-indicator area                        optional, on by default · the bottom safe-area inset, Background/Primary
```

| Part | Required | Description |
|---|---|---|
| **Root** | Required | The public master: fills the host's width (iOS `Flexible`, Android `FillMax`), hugs its height, `s8` on every side of the card (Figma `container`). The component does not position itself — the host pins it to the screen's edge (§ 9) |
| **Overlay** | Optional, on by default | Figma `.background-overlay`: a `Background/Primary` fill under a linear mask, transparent at the top of the root's `s8` frame and opaque at its bottom — the content that scrolls under the card fades into the screen's background instead of meeting the card at a hard line. It lies under the card (the frame's auto layout stacks the first child on top; the 3.1 brief: "The overlay is placed under the component"). On the web, the root's `::before` |
| **Card** | Required | Figma `FloatingContainer` (`36135:19436`): a vertical auto layout, `s4` padding, `s24` corners (max ×130%), a `Surface/Floating` fill (the master binds `Background/Primary`; the DS default follows Floating Button — owner, 2026-11-12), a `Border/Transparent` stroke of 1 drawn outside, the `Elevation (shadows)/M` effect style. Its width fills the root, its height hugs the content |
| **Content** | Required | The card's `slot`: whatever DS components the host puts in — a Button, two Buttons side by side, a Cell, a TextField. A free slot: vertical, centred, no gap of its own, min-height `s24` — the host lays out its own content. On the web, the card's children |
| **Home-indicator area** | Optional, on by default | Figma `.Home Indicator iOS` (`Background color = Primary`): the 34-high strip under the iOS home indicator, filled `Background/Primary`. The pill itself is system chrome, not the component's. On the web, the root's bottom padding grows by `env(safe-area-inset-bottom)` — the device's real inset, zero where there is none |

- The master is drawn for the **bottom edge**: the overlay fades downwards, into the screen, and the
  home-indicator area sits below the card. The frame's chat example also uses the container for a
  floating header; that header is built from frames in the frame, not from the master, so a
  top-edge form is not specified (flagged, § 4).

### RTL layout

The Figma component stages no RTL. The container is symmetric — nothing in it mirrors; the content
inside follows the reading direction (`dir="rtl"`) on its own contract.

---

## 3. Variants and sizes

| Axis | Values | Default | Web mechanism |
|---|---|---|---|
| **Preset** | `l` · `custom-floating-container` | `l` | One class, `.floating-container`, carrying the L values. The custom base re-points the card's custom properties on the instance (below); `data-ds-preset` names which one it is |
| **Overlay** | on · off | on | `.floating-container--no-overlay` removes the gradient |
| **Home Indicator** | on · off | on | `.floating-container--no-home-indicator` removes the bottom inset (and its fill) |

**L-size and CustomFloatingContainer carry the same numbers** — the frame's two Layout blocks are
identical (`s4` / `s4` / `s1` / `s24`). The difference is what may move: on the preset nothing does;
on the custom base each value is "by default" (the frame's wording), and a design request may
re-point it to another DS token. The re-pointable values and their custom properties:

| Value | Custom property | Default |
|---|---|---|
| Background colour | `--floating-container-fill` | `var(--surface-floating)` |
| Border colour | `--floating-container-border` | `var(--border-transparent)` |
| Shadow style | `--floating-container-shadow` | `var(--shadow-m)` |
| Padding | `--floating-container-padding` | `var(--sp-100-s4)` |
| Corner radius | `--floating-container-radius` | `var(--sp-130-s24)` |

- The border's width (1) and position (outside) are not re-pointed: the SP scale has no `s1` step to
  re-point it to.
- One size. Android's `DsFloatingContainer.Style` has only `L`; the frame shows no other.
- The Figma booleans `Overlay` and `Home Indicator` default to on; the frame's Styles and Layout
  specimens turn `Home Indicator` off, its examples use each combination.

---

## 4. States

FloatingContainer has one state: **Standard**. It is not interactive; the Figma component stages no
State axis.

| State | What shows |
|---|---|
| **Standard** | The card on its elevation, `s8` from the root's edges, the overlay behind it and the home-indicator area below it (each when on) |

- The colours are defaults, not states: the custom base re-points them (§ 3).
- No Pressed, Hover, Disabled or Skeleton state. The content's components keep their own states — a
  disabled Button inside stays a disabled Button; a loading screen shows its components' skeletons
  inside the card.
- The card's press with a lone Button is a motion step, not a state (§ 5).
- Figma flags:
  - The Specification frame's header says **Version 3.0**; the master is `[FloatingContainer] 3.1`.
  - The frame's copy is in Russian, and its sidebar lists States, RTL, Animation and Accessibility
    sections the frame does not have (it holds Description, Styles, Layout, Usage context and
    Examples).
  - The master mixes platforms: the overlay instance is `OS = Android`, the home indicator is the iOS
    one. The two `OS` variants of `.background-overlay` are identical, so nothing visible follows.
  - The chat example's floating header is built from frames (`.DockedSlot`), not from the master.
  - The master binds the card to `Background/Primary` (grey-875 in Night), while a Floating Button
    on the map is `Surface/Floating` (grey-870) and the frame calls them "one style". The DS default
    is `Surface/Floating` (owner, 2026-11-12); the master is to be re-bound.

---

## 5. Animation and behavior

Motion follows [`motion-rules.md`](https://super-dollop-pzmo65r.pages.github.io/motion.md). The Specification frame
has no Animation section; the press below is an owner ruling (2026-11-12) on the catalogue's
existing moment "A button or a chip is pressed" (`press-button`, § 7).

| Event | Pattern | Token | CSS |
|---|---|---|---|
| The card's one Button is pressed | `pushButton` | `--component-push-button-press` | `.floating-container__card` → `transform: scale(0.95)`; the Button holds still inside it |
| Released | `pushButton` | `--component-push-button-release` | Back to `scale(1)` |

- **One shape presses.** When the card holds a single Button, the card takes the Button's press —
  the same tokens, so the two stay in step — and the Button does not scale on its own. Left to
  itself, the Button would shrink away from the card's edge and open a gap round it inside a card
  that stays still.
- The card follows the Button's eligibility: a disabled or Skeleton Button presses neither.
- With two or more components in the card, each presses on its own contract and the card stays
  still.
- The press is visual only: the card gains no handler, role or focus stop
  (`floating-container-a11y.md`).
- Web: `:has()` on the card. Native builds — to align: neither bridge records a press on the
  container (flagged).
- It appears and leaves with its screen, on the screen's transition.
- The content's other motion is its own — a Cell's highlight, a TextField's focus.
- The card re-hugs a content change in the same frame; nothing interpolates.
- A container shown or hidden while its screen stays — on scroll, after a choice — is the host's
  call: the DS specifies no motion for it (owner, 2026-11-12).
- Reduced motion: `motion.css` zeroes both tokens — the card's press is instant, as the Button's; no
  override of its own.

---

## 6. Color tokens

| Element | Token | Notes |
|---|---|---|
| Card fill | `var(--surface-floating)` | `Surface/Floating` — white in Day, grey-870 in Night, one fill with Floating Button (owner, 2026-11-12; the Figma master still binds `Background/Primary`); re-pointable on the custom base (`--floating-container-fill`) |
| Card border | `var(--border-transparent)` | Figma `Border/Transparent`, 1 outside; re-pointable (`--floating-container-border`) |
| Card shadow | `var(--shadow-m)` | Figma `Elevation (shadows)/M` — 0 4 16, 8 % of grey; re-pointable (`--floating-container-shadow`) |
| Overlay | `var(--background-primary)` | A linear gradient from transparent (top) to opaque (bottom) — the mask's stops are 0 / 50 / 100 % at 0 / 0.5 / 1 opacity, a straight ramp. 3.0 used `Background/BaseWhite`; 3.1 binds `Background/Primary` (ZCP-3120) |
| Home-indicator area | `var(--background-primary)` | Solid |

- The overlay and the home-indicator area are `Background/Primary` — on a `Background/Primary`
  screen they melt into it in Day and Night alike. On a screen of another background
  (`Background/BaseGrey`, `Background/Secondary`) they stay `Background/Primary`: the master binds
  them, and neither belongs to the custom base.
- On a `Background/Primary` screen the card fill equals the screen's in Day and is a step lighter in
  Night (grey-870 on grey-875); the card is told from the screen by its border and its shadow. The components inside are read on the card's fill, whatever the
  screen's background; a re-pointed fill re-checks their contrast (`floating-container-a11y.md`).

---

## 7. Typography

FloatingContainer has no text of its own. The content's components carry their own text styles and
scaling.

---

## 8. Spacing

Each value takes the SP family Figma binds: `SP/100% ratio` → `var(--sp-100-sN)` (fixed),
`SP/130% ratio` → `var(--sp-130-sN)` (cap130).

| Property | Token | @100% | Figma | Notes |
|---|---|---|---|---|
| Root padding (around the card) | `var(--sp-100-s8)` | 8 | `SP/100% ratio/s8` | All four sides (Figma `container`) |
| Card padding | `var(--sp-100-s4)` | 4 | `SP/100% ratio/s4` | "s4 — vertical padding", "s4 — horizontal padding" |
| Card radius | `var(--sp-130-s24)` | 24 | `SP/130% ratio/s24` | "s24 — corners size", "s24 / (max*130%)" |
| Card border | `1px` | 1 | unbound | "s1 — border size"; outside. The SP scale has no `s1` step — a hairline, as on Floating Button |
| Content min-height | `var(--sp-100-s24)` | 24 | `SP/100% ratio/s24` | The slot's minimum height; the card is 32 high at least |
| Content gap | — | 0 | `SP/100% ratio/s0` | The slot adds none — the host lays out its components |
| Home-indicator area | `env(safe-area-inset-bottom)` | 34 in Figma | — | The iOS home-indicator strip; on the web the device's inset, zero where there is none |

- The card's radius grows to 130% and stops; its padding and the root's margins stay fixed. The
  content's components scale on their own contract, and the card hugs them.
- The card's width fills the root: the root's width minus 2 × `s8`.

---

## 9. Usage context

### When to use FloatingContainer

From the Specification frame:

- The main content stays visible — a map, cards, media, the camera, navigation.
- An action stays available throughout.
- Controls appear over the content without blocking it entirely.

Recommendations (the frame):

- One main action inside the container.
- No large or scrolling content in it.
- A compact container, focused on actions.
- Content that reads in both the light and the dark theme.

The frame's examples:

- **Above the TabBar** — extra floating elements over the TabBar.
- **On the map** — the main action button, in one style with the Floating Buttons on the map.
- **On the chat screen** — a floating header and the message field.

### When not to use

- For one action that an icon, or an icon with a label, carries — [Floating Button](https://super-dollop-pzmo65r.pages.github.io/floating-button.md).
  The frame: the container is for where a Floating Button cannot be applied.
- For a short message that comes and goes — [Snackbar](https://super-dollop-pzmo65r.pages.github.io/snackbar.md).
- For long or scrolling content — a bottom sheet.

### Placement

- The host pins the container to the screen's bottom edge (on the web `position: fixed` or
  `sticky` with `bottom: 0`) and gives the content it covers room to scroll clear of it — a bottom
  inset as tall as the container, so the last item can come up above the card — and brings a
  focused element into view above it (§ 10).
- With a TextField inside, or at the largest text sizes, a pinned container can leave little of the
  content in view; the host then lets it scroll with the content instead of pinning it.
- Two buttons side by side share the card's width equally — each on Button's `Width = fill`, the
  host's row laying them out — so no empty space is left at the sides.
- With the overlay on, the content fades into the screen's background where it passes under the
  card's margins.
- `Home Indicator` on where the container meets the screen's bottom edge; off where something sits
  below it (a TabBar).
- On the web `env(safe-area-inset-bottom)` is non-zero only when the page opts into the full screen
  (`<meta name="viewport" content="…, viewport-fit=cover">`); without it the browser keeps the page
  above the home indicator and the strip is zero — the host's choice.

### Related components

- [Floating Button](https://super-dollop-pzmo65r.pages.github.io/floating-button.md) — the floating control for a single action.
- [Button](https://super-dollop-pzmo65r.pages.github.io/button.md) — the usual content.
- [Snackbar](https://super-dollop-pzmo65r.pages.github.io/snackbar.md) — the floating message.

---

## 10. Accessibility

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

- **Not a control.** The container has no role, no name and no focus stop; assistive technology
  meets its components one by one, in their order — a group, never merged into one element.
- **Read in place.** The components inside are reached where the container sits in the screen's
  reading order — after the content for a bottom container — and as one group.
- **Keeps the focus clear.** The content scrolling under the container gets a bottom inset as tall
  as the container, so the last item can scroll above the card, and the host brings a focused
  element into view above it (WCAG 2.4.11 for keyboard focus; the same intent for screen-reader
  focus) — on the web the inset does both; on iOS and Android a screen reader does not know about
  the card, so the host scrolls a focused item clear.
- **Decoration is silent.** The overlay, the shadow and the home-indicator area are not announced.
- **The press is visual only.** The card presses with a lone Button and gains no handler, role or
  focus stop; reduced motion makes the press instant.
- **Text scaling** — the card's corners grow to 130%, its margins stay; the content scales on its
  own contract and the card hugs it.

---

## 11. Analytics and coverage contract

Contract per [`prototype-analytics-and-coverage.md`](https://github.com/inDriver/oymyakon-ds/blob/main/docs/prototype-analytics-and-coverage.md).

| Field | Value |
|---|---|
| `data-ds-component` | `floating-container` |
| Coverage unit | yes — one root, one DS component instance |
| Tap target model | none — the components inside carry their own targets |
| Actions | none |
| Internal targets | none — each component inside is its own coverage unit |
| Emits value | no |

Attribute model: `data-ds-preset` = `custom-floating-container` when any `--floating-container-*`
property is re-pointed on the instance, `l` otherwise; `data-ds-state` = `standard`. The `--no-overlay` / `--no-home-indicator`
modifiers are layout chrome, not presets.

```html
<!-- L, overlay and home-indicator area on: one primary Button inside -->
<div class="floating-container"
     data-ds-component="floating-container" data-ds-component-id="checkout.floating-cta"
     data-ds-preset="l" data-ds-state="standard">
  <div class="floating-container__card">
    <button class="button button--l button--primary" type="button"
            data-ds-component="button" data-ds-component-id="checkout.pay"
            data-ds-preset="l" data-ds-variant="primary" data-ds-action="tap">…</button>
  </div>
</div>

<!-- Above a TabBar: no home-indicator area -->
<div class="floating-container floating-container--no-home-indicator" … data-ds-preset="l">
  <div class="floating-container__card">…</div>
</div>

<!-- The custom base: a re-pointed corner radius -->
<div class="floating-container" style="--floating-container-radius: var(--sp-130-s20)"
     … data-ds-preset="custom-floating-container">
  <div class="floating-container__card">…</div>
</div>
```

The host pins the root and gives the content under it a matching bottom inset (§ 9). The preview
page is the reference.

---

## Changelog

| Version | Date | Change |
|---|---|---|
| 3.1.1 | 2026-11-12 | Owner rulings, and Status Draft → Ready (owner). **The card's default fill is `Surface/Floating`** — one fill with Floating Button (white in Day, grey-870 in Night; was `Background/Primary`, grey-875 in Night); the overlay and the strip stay `Background/Primary`; the Figma master is to be re-bound. Night contrast on the card re-measured: `TextAndIcon/Primary` 15.52:1, `Secondary` 7.21:1. A container shown or hidden while its screen stays is the host's call — no DS motion. |
| 3.1.0 | 2026-11-12 | Initial spec, from the Figma `[FloatingContainer] 3.1` page (`15691:39482`): the public master `15691:40935` (the `s8` wrapper with the `Overlay` and `Home Indicator` booleans), the card `FloatingContainer` `36135:19436` (one slot), the Specification frame `15691:41819` and the brief "Update 3.1" `36840:11574` (ZCP-3120 — the overlay's colour `Background/BaseWhite` → `Background/Primary`). One preset, L, on the CustomFloatingContainer base — the same numbers, the base's re-pointable through five custom properties; one state, Standard. Spacing takes the SP family Figma binds: the margins, the padding and the slot's min-height fixed, the radius cap130; the border a 1 hairline outside (no `s1` on the scale). Web: the root is the master (the overlay its `::before`, the home-indicator strip `env(safe-area-inset-bottom)`), the host pins it. Both platforms ship `DsFloatingContainer`; divergences recorded. Review pass (spec + a11y, same day): a traversal group, never merged (the traps named per platform); the focus contract split into the bottom inset (the last item scrolls clear) and the host bringing a focused item into view above the card — on iOS / Android the screen reader does not know about the card; the content read on the card's own fill, contrast measured; the `data-ds-preset` rule stated. Page review: the map example reads "in one style with the Floating Buttons on the map" (the frame's wording — "in one row" was a mistranslation); `TextAndIcon/Secondary` 7.92:1 Night, as `color-rules.md`. Owner review (2026-11-12): the card presses with a lone Button — `pushButton`, the Button holding still inside it, so one shape presses (owner ruling; disabled and Skeleton Buttons press neither; native builds to align); two buttons stretch across the card on Button's `Width = fill`; the page drops the large-content counter-example from Usage (the frame's recommendation stays in § 9). A11y re-check: `Scaffold`'s `bottomBar` is an inset, not a viewport end; the focus triggers named (`@AccessibilityFocusState`; Compose has no accessibility-focus callback); WCAG 2.4.11 scoped to keyboard focus, 1.4.4 beside 1.4.10; `clearAndSetSemantics` removes rather than fuses; the root's box takes the pointer — only the overlay passes it; iOS Safari's keyboard and `visualViewport`; reduced motion makes the press instant. |

---

# FloatingContainer — Accessibility

FloatingContainer is a container, not a control. It has no role, no name, no value and no focus stop
of its own: assistive technology meets the components inside it one by one, each on its own
contract — a Button inside is announced as that Button. The card's background, its shadow and
border, the gradient overlay and the home-indicator strip are drawing and padding, not announced.

What the container owes is placement:

- **Reading order.** Its components are read together, as one group, at the place the container
  holds in the screen's reading order — for a bottom container, after the content it floats over.
  Floating over the content is a visual position; it does not move the container earlier in the
  reading order, and the items scrolling under it are not interleaved with its components. Inside the
  card the components are read in layout order — start to end, top to bottom; the start follows the
  reading direction where a component supports RTL.
- **The focus stays clear of the card** (WCAG 2.2 · 2.4.11 Focus Not Obscured, AA — keyboard focus
  on the web; the same intent for screen-reader focus on iOS and Android, which the criterion does
  not literally cover). Two parts, both the host's:
  1. The content that scrolls under the container gets a bottom inset as tall as the container — the
     last item can scroll above the card.
  2. A focused element is brought into view above the card. On the web the inset does this too
     (`scroll-padding-bottom`); on iOS and Android a screen reader reveals an item against the
     scroll view's bounds, which do not know about the card, so an item lying wholly under it can take
     the focus without a scroll — the host scrolls it clear when it takes the focus, or ends the
     scroll view above the card (at the cost of the overlay's fade-under).

- **The press is visual only.** With one Button inside, the card presses with it
  (`floating-container.md` § 5) — drawn from the Button's own press state. The card gains no handler,
  no role and no focus stop: the Button stays the only target, announced on its own contract.

The Specification frame has no Accessibility section; this contract is the DS's.

---

## Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Container (root) | — | — | None — a traversal group, not a focus stop | — |
| Card background, border, shadow | — | — | Drawing only — not a node | — |
| Overlay | — | — | Drawing only — not a node | — |
| Home-indicator strip | — | — | Padding only — not a node | — |
| Components inside | Each component's own | Each component's own | Each component's own | Each component's own |

- **The group:** `Modifier.semantics { isTraversalGroup = true }` on the container keeps its
  components together (Compose 1.5 or later).
- **The order is the host's:** `traversalIndex` is relative to the siblings — where the default
  positional order would interleave the container with the items under it, the host gives it a
  higher index than the content's default `0f`, so the group is read after it. To verify with
  TalkBack.
- **The trap:** `Modifier.semantics(mergeDescendants = true)` on the container fuses its components
  into one node — a Button and a TextField become one stop, neither reachable on its own;
  `clearAndSetSemantics` removes them from the tree altogether. The container is a traversal group,
  never a merged node. The card's press takes no `Modifier.clickable` either — it follows the
  Button's own press.

### Edge states

- **The focus stays clear of the card:** the scrolling content takes a bottom content padding equal
  to the container's measured height (`LazyColumn(contentPadding = …)`, or the scroll container's
  bottom padding) — the last item can scroll above the card. `Scaffold`'s `bottomBar` is this same
  case: the content still runs behind the bar and receives its height as `innerPadding`, which it
  applies as that bottom content padding. An item wholly under the card can still take TalkBack's
  focus without a scroll: the host brings it into view above the card when it is focused
  (`BringIntoViewRequester`, `LazyListState.animateScrollToItem`), or ends the scroll viewport above
  the card (a `Column` with the content `weight(1f)` and the container below it). Compose exposes no
  accessibility-focus callback; where the host cannot observe it, ending the viewport above the card
  is the dependable form. To verify on a device.
- **A TextField inside:** the container moves above the keyboard with the IME inset
  (`WindowInsets.ime` — `Modifier.imePadding()`, edge-to-edge or `adjustResize`). Where the
  container and the keyboard leave little of the content in view — the largest font scales, a long
  field — the host lets the container scroll with the content instead of pinning it.
- **Disabled / Error / Skeleton:** not applicable to the container — one state, Standard. A disabled
  component inside is announced "Disabled" and stays in swipe navigation; a loading one keeps its own
  announcement.
- **Content change:** the container is not a live region. A component inside that changes (a CTA
  whose price updates) announces on its own contract, or the host does.
- **Text scaling:** the card's corners grow to 130 % and stop, its margins stay; the components scale
  on their own contract and the card hugs them. The bottom inset follows the container's measured
  height at every scale.

---

## iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Container (root) | — | — | None — a group only | — |
| Card background shape (a `.background` / `.overlay` view) | — | — | `accessibilityHidden(true)` | — |
| Card content wrapper (the view that holds the components) | — | — | Not hidden — hiding it hides every component inside | — |
| Overlay | — | — | `accessibilityHidden(true)` | — |
| Home-indicator strip | — | — | Safe-area padding — not an element | — |
| Components inside | Each component's own | Each component's own | Each component's own | Each component's own |

- **The group:** `accessibilityElement(children: .contain)` — the container groups its components,
  which stay separate elements.
- **The order:** a container attached with `safeAreaInset(edge: .bottom)` comes after the content, in
  the view order and by position. A container laid over the content in a `ZStack` is read after it
  when it is declared after it; declared earlier, it takes an `accessibilitySortPriority` lower than
  the content's (higher is read first) — set by the host, since the priority is relative to the
  siblings.
- **The trap:** `accessibilityElement(children: .combine)` on the container fuses its components into
  one element, and `accessibilityHidden(true)` on the content wrapper removes them all. The container
  is `.contain`, never `.combine`, and only its background shape is hidden. The card's press takes no
  `Button` wrapper round the card — it follows the Button's own press.

### Edge states

- **The focus stays clear of the card:** `safeAreaInset(edge: .bottom)` insets the scroll view's
  content by the container's height — the last item can scroll above the card; a container laid over
  the content another way sets the scroll view's bottom content inset to its measured height. An item
  wholly under the card can still take VoiceOver's focus without a scroll: the host scrolls it clear
  when it takes the VoiceOver focus (`@AccessibilityFocusState`, iOS 15 or later, with
  `ScrollViewReader`'s `scrollTo(_:anchor:)`), or ends the scroll view above the card. To verify on
  a device.
- **A TextField inside:** as on Android — the container rides above the keyboard; where little
  content stays in view, the host lets it scroll with the content.
- **Disabled / Error / Skeleton:** not applicable to the container. A disabled component inside is
  announced "Dimmed" and stays reachable with swipe navigation.
- **Content change:** as on Android.
- **Dynamic Type:** the corners to 130 %, the margins fixed, the card hugging its components at every
  size; the bottom inset follows the container's measured height.

---

## Web preview (reference)

- **No role, no name.** The root and the card are `<div>`s with no `role`, no `aria-label` and no
  `tabindex`. The overlay is the root's `::before` with no text — outside the accessibility tree.
- **DOM order is the reading order.** A bottom container's markup follows the content it floats
  over; `position: fixed` / `sticky` moves it visually only, so the DOM order is the reading and Tab
  order (WCAG 1.3.2, 2.4.3). Inside the card, the components' DOM order is the layout order.
- **The focus stays clear of the card** (WCAG 2.4.11, keyboard focus). The scroll container under the container —
  `html` when the document itself scrolls — gets `padding-bottom` and `scroll-padding-bottom` equal to
  the container's height: the last item scrolls clear of the card, and a Tab or a `focus()` brings the
  focused element into view above it. The height is measured, not assumed — it changes with the
  content and the text size (the preview page measures it with a `ResizeObserver`).
- **The home-indicator strip** is `env(safe-area-inset-bottom)` — present on a device with a home
  indicator when the page opts into `viewport-fit=cover`, zero elsewhere; it holds nothing.
- **Forced colours:** the border and the shadow are `box-shadow`, which forced colours remove; the
  card takes a `CanvasText` outline so its edge survives. The edge carries no contrast requirement
  (below).
- **Zoom, reflow and the keyboard** (WCAG 1.4.10 Reflow for the 400 % zoom, 1.4.4 Resize Text for
  the text size): at 400 % zoom (320 CSS px wide), at the largest text sizes, or with a TextField
  inside and the on-screen keyboard open, a pinned container takes a large share of the viewport.
  Where little of the content stays in view, the host takes the container out of the fixed position
  and lets it scroll with the content (in flow, or `sticky` inside the content).
- **The keyboard on iOS Safari:** a `position: fixed` bottom container stays on the layout viewport
  and does not ride the on-screen keyboard — the host sizes it against `visualViewport`, or unpins it
  (above).
- **The overlay takes no pointer events of its own** (`pointer-events: none`); the root's box — the
  margins and the home-indicator strip — still sits over the content and takes them, so what lies
  under the root is reached by scrolling it clear, as for the focus.
- **Reduced motion:** the container's one motion is the card's press with a lone Button, on
  `--component-push-button-press` / `--component-push-button-release`. `motion.css` zeroes both under
  `prefers-reduced-motion`: the card still steps to 95 % and back, instantly, as the Button does —
  nothing is announced, and the press is not a state.

### Contrast

- The container has no text and is not a control: the card's edge — `Border/Transparent` and the M
  shadow — carries no 3:1 requirement (WCAG 1.4.11 covers controls and graphics needed to understand
  the content; the card is neither).
- The components inside are read on the card's own fill, `Surface/Floating` — not on the screen's
  background, which may differ (a `Background/BaseGrey` or `Background/Secondary` screen keeps a
  `Surface/Floating` card). On it (WCAG 1.4.3; measured from the primitives, WCAG 2 relative
  luminance):
  - `TextAndIcon/Primary` — 17.76:1 Day, 15.52:1 Night.
  - `TextAndIcon/Secondary` — 5.25:1 Day, 7.21:1 Night.
- An instance that re-points `--floating-container-fill` re-checks the pairs of every component
  inside, in both themes.
- The overlay fades content passing under the card's margins; that content is read once it scrolls
  clear, which the bottom inset makes possible for every item.

---

## Checklist

- [ ] The container has no role, name or focus stop; its components are announced on their own contracts
- [ ] Its components are read together, after the content for a bottom container (DOM order on the web); never merged or combined into one element
- [ ] The content under it has a bottom inset equal to its measured height — the last item scrolls clear of the card
- [ ] A focused element is brought into view above the card — tested with TalkBack and VoiceOver on a long list, an item lying wholly under the card
- [ ] The card's content wrapper is not hidden from assistive technology — only its background shape
- [ ] The card's press follows the lone Button's own press — the card has no handler, role or focus stop, and does not press while the Button is disabled or a Skeleton
- [ ] The overlay, the card's border and shadow and the home-indicator strip are not announced
- [ ] With a TextField inside, the container clears the keyboard and the content stays in view
- [ ] Forced colours keep the card's edge (a `CanvasText` outline)
- [ ] The card hugs its components at every text size; the corners stop at 130 %

---

## Machine contract — `specs/components/floating-container/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": "floating-container",
  "name": "FloatingContainer",
  "version": "3.1.1",
  "description": "An elevated rounded card that floats over the screen's content and holds DS components (a CTA, a pair of buttons, a short group of controls) — s8 margins, a gradient overlay fading the content under it and the bottom safe-area strip, both on by default. A container, not a control: no state, no action, no text of its own.",
  "files": {
    "spec": "specs/components/floating-container/floating-container.md",
    "a11y": "specs/components/floating-container/floating-container-a11y.md",
    "preview": "src/floating-container.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon (components) — main file",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {
      "[FloatingContainer] 3.1": {
        "key": "b5572c1d68c9c6fed3bacc626bf55ce9f269a951",
        "note": "A single component (no variant set), node 15691:40935 on page 15691:39482: the s8 wrapper with the booleans Overlay and Home Indicator. Specification frame 15691:41819; brief \"Update 3.1\" 36840:11574 (ZCP-3120)."
      },
      "FloatingContainer": {
        "key": "fac3d322548cb855664f0d45b4cefb5fd209de56",
        "note": "The card (node 36135:19436, section Building blocks 15691:46590) — one SLOT; the CustomFloatingContainer base the L-size preset is built on."
      }
    },
    "capturedAt": "2026-11-11"
  },
  "root": {
    "class": "floating-container",
    "dataDsComponent": "floating-container"
  },
  "anatomy": {
    "overlay": {
      "optional": true,
      "notes": "Pseudo-element: .floating-container::before — not an element of its own: Background/Primary from transparent (top) to opaque (bottom of the s8 frame), under the card. Figma .background-overlay. Decorative; never in the accessibility tree."
    },
    "card": {
      "class": "floating-container__card",
      "notes": "Figma FloatingContainer: Surface/Floating (the DS default, owner 2026-11-12; the master binds Background/Primary), Border/Transparent 1px outside, shadow M, s4 padding, radius s24 cap130; fills the root's width, hugs its content (min-height s24 for the content)."
    },
    "content": {
      "notes": "No wrapper of its own: the children of .floating-container__card — Figma's free slot (vertical, centred, no gap): the host's DS components, each its own coverage unit with its own contract."
    },
    "homeIndicatorArea": {
      "optional": true,
      "notes": "No element of its own: .floating-container's bottom padding grown by env(safe-area-inset-bottom) and filled Background/Primary — Figma .Home Indicator iOS (34 high). The pill is system chrome."
    }
  },
  "axes": {
    "preset": {
      "title": "Preset",
      "type": "enum",
      "values": ["l", "custom-floating-container"],
      "default": "l",
      "css": {
        "mechanism": "one class, .floating-container, carries the L values; the custom base re-points the card's custom properties (fill, border, shadow, padding, radius) on the instance. data-ds-preset names which one it is"
      },
      "figma": {
        "kind": "none",
        "notes": "The frame's Styles: CustomFloatingContainer (the base) and L-size (the one preset) — the same master, the same numbers. Android DsFloatingContainer.Style has only L."
      },
      "constraints": [
        "On l nothing moves; on custom-floating-container each card value is a default a design request may re-point to another DS token (not a raw value).",
        "data-ds-preset is custom-floating-container when any --floating-container-* property is re-pointed on the instance, l otherwise."
      ]
    },
    "overlay": {
      "title": "Overlay",
      "type": "boolean",
      "default": true,
      "css": { "modifier": "floating-container--no-overlay", "mechanism": "the modifier removes the gradient (off)" },
      "figma": { "kind": "variant-property", "property": "Overlay", "values": { "true": "true", "false": "false" }, "notes": "A boolean component property (Overlay#15691:9), not a variant — the visibility of .background-overlay. Neither platform has a parameter for it (decoration)." }
    },
    "homeIndicator": {
      "title": "Home Indicator",
      "type": "boolean",
      "default": true,
      "css": { "modifier": "floating-container--no-home-indicator", "mechanism": "the modifier removes the bottom safe-area strip (off)" },
      "figma": { "kind": "variant-property", "property": "Home Indicator......................................................", "values": { "true": "true", "false": "false" }, "notes": "A boolean component property (#15691:8), not a variant — the name is padded with dots, copied verbatim; the visibility of .Home Indicator iOS. Neither platform has a parameter for it (padding)." },
      "constraints": [
        "Off where something sits below the container (a TabBar); on where the container meets the screen's bottom edge."
      ]
    },
    "fill": {
      "title": "Background colour",
      "type": "token",
      "tokens": ["--surface-floating"],
      "customizable": "any DS colour token, on the custom base only — the content keeps its contrast on it in both themes",
      "default": "--surface-floating",
      "css": { "customProperty": "--floating-container-fill" },
      "figma": { "kind": "none", "notes": "The master binds Background/Primary; the DS default is Surface/Floating — one fill with Floating Button (owner, 2026-11-12); the master is to be re-bound." }
    },
    "border": {
      "title": "Border colour",
      "type": "token",
      "tokens": ["--border-transparent"],
      "customizable": "any DS border token, on the custom base only",
      "default": "--border-transparent",
      "css": { "customProperty": "--floating-container-border" },
      "figma": { "kind": "none", "notes": "The card's stroke binds Border/Transparent (\"by default\"), 1 outside." }
    },
    "shadow": {
      "title": "Shadow style",
      "type": "token",
      "tokens": ["--shadow-m"],
      "customizable": "any DS elevation token (--shadow-s / -m / -l), on the custom base only",
      "default": "--shadow-m",
      "css": { "customProperty": "--floating-container-shadow" },
      "figma": { "kind": "none", "notes": "The card's effect style Elevation (shadows)/M (\"by default\")." }
    },
    "padding": {
      "title": "Padding",
      "type": "tokenScale",
      "scale": "sp-100",
      "default": "--sp-100-s4",
      "css": { "customProperty": "--floating-container-padding" },
      "figma": { "kind": "layout", "notes": "The card's padding, SP/100% ratio/s4 on all sides." },
      "notes": "A default on the custom base, locked on l."
    },
    "radius": {
      "title": "Corner radius",
      "type": "tokenScale",
      "scale": "sp-130",
      "default": "--sp-130-s24",
      "css": { "customProperty": "--floating-container-radius" },
      "figma": { "kind": "layout", "notes": "The card's corners, SP/130% ratio/s24 — \"s24 / (max*130%)\"." },
      "notes": "A default on the custom base, locked on l."
    }
  },
  "states": {
    "default": ["standard"]
  },
  "analytics": {
    "dataDsComponent": "floating-container",
    "action": "none",
    "notes": "Coverage unit. No tap target — the components inside carry their own and are their own coverage units. data-ds-preset = l | custom-floating-container; data-ds-state = standard. The --no-overlay / --no-home-indicator modifiers are layout chrome, not presets."
  },
  "rtl": {
    "supported": true,
    "notes": "Symmetric — nothing mirrors; the content follows the reading direction on its own contract. Figma stages no RTL."
  },
  "constraints": [
    "One state, Standard: no pressed, hover, disabled or skeleton state of its own; it appears and leaves with its screen, and the components inside keep their own states and motion.",
    "Press (owner, 2026-11-12): a card holding exactly one Button presses with it — the card takes transform: scale(0.95) on --component-push-button-press / --component-push-button-release and the Button holds still inside it; a disabled or Skeleton Button presses neither, and with two or more children the card stays still. Visual only: the card gains no handler, role or focus stop. Native builds — to align.",
    "Not a control: no role, no name, no focus stop; its components are read together, after the content for a bottom container (DOM order on the web).",
    "The host positions it (pinned to the screen's bottom edge), gives the content under it a bottom inset equal to its measured height (the last item scrolls clear) and brings a focused element into view above the card (WCAG 2.4.11) — on iOS and Android the screen reader does not know about the card.",
    "A traversal group, never a merged node: Android isTraversalGroup (not mergeDescendants), iOS accessibilityElement(children: .contain) (not .combine); only the card's background shape is hidden, never the wrapper holding the components.",
    "One main action, compact, no large or scrolling content inside (the frame's recommendations); a single icon or icon + label action is a Floating Button instead.",
    "The card border is a 1px hairline drawn outside — the SP scale has no s1 step, so its width is not re-pointable.",
    "The master is drawn for the bottom edge (the overlay fades downwards, the home-indicator strip below); a top-edge form is not specified.",
    "Both platforms ship DsFloatingContainer (iOS DS3, Android core.compose.ds.v3.container; Android's bridge names [FloatingContainer] 3.0) with DsCustomFloatingContainer for the custom base; neither has an Overlay or Home Indicator parameter."
  ]
}
```
