v3.1.0
FloatingContainer

An elevated 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.

DS3 3.0 -- 3.1.1
Thai Rice
Ready meal
$6.50
Pasta Carbonara
Ready meal
$8.20
Tuna Sandwich
Breakfast
$4.90
Club Sandwich
Breakfast
$5.40
Fried Potatoes
Sides
$3.10
Breaded Chicken
Ready meal
$7.30
Checkout

Overview

FloatingContainer 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. It is used where a Floating Button cannot be applied.

  • 1
    L-size
  • 2
    CustomFloatingContainer
Checkout
1
Checkout
2

States

One state — Standard. FloatingContainer is not interactive.

Standard

1
2
3
  • 1
    Card --surface-floating — the fill: white in Day, grey-870 in Night — one fill with Floating Button.--border-transparent — the 1px border, drawn outside.--shadow-m — the elevation.--floating-container-fill — re-points the fill on the custom base.--floating-container-border — re-points the border colour.--floating-container-shadow — re-points the shadow style.
  • 2
    Overlay --background-primary — transparent at the top of the root, opaque at the bottom of its margin; under the card, so the content scrolling beneath fades out before it meets the card.
  • 3
    Home-indicator strip --background-primary — solid, under the iOS home indicator; drawn here at the Figma height, 34.

On a grey screen

1
2
  • 1
    Card --surface-floating — stays the card's fill: the components inside are read on it, not on the screen.--background-base-grey — the screen in this example; any screen background other than Primary alike.
  • 2
    Overlay and strip --background-primary — stay as the master binds them: the grey fades to white round the card. Neither belongs to the custom base.

Anatomy

The public master: the card on its margin, the overlay behind it and the home-indicator strip under it.

1
2
3
4
5
  • 1
    Root var(--sp-100-s8) — round the card, on every side.Fills the host's width and hugs its height; the host pins it to the screen's bottom edge.Required.
  • 2
    Overlay --background-primary — transparent to opaque, behind the card: the root's ::before.Optional, on by default — floating-container--no-overlay turns it off.
  • 3
    Card var(--sp-100-s4) — from the card's edge to its content.Figma FloatingContainer: fills the root's width, hugs its content.Required.
  • 4
    Content var(--sp-100-s24) — the minimum height of the slot.The host's DS components — a Button, two Buttons, a Cell, a TextField. A free slot: centred, no gap of its own; a fill-width component fills it.Required.
  • 5
    Home-indicator strip --background-primary — solid, as tall as the device's bottom inset (env(safe-area-inset-bottom)); the pill is system chrome.Optional, on by default — floating-container--no-home-indicator turns it off.

Layout

The root fills its host's width and hugs its height: the card on its margin, the home-indicator strip under it.

Structure

  • Root var(--sp-100-s8) — the margin round the card, on every side; 8 in every SP mode.Width — fills the host: Flexible on iOS, FillMax on Android, the host's full width on the web. Height — hugs.The host positions it — pinned to the screen's bottom edge; the root sets no position of its own.
  • Home-indicator strip env(safe-area-inset-bottom) — the strip's height: the device's bottom inset, zero where there is none.34 — the iOS strip in Figma, drawn here.On the web the inset is non-zero only when the page sets viewport-fit=cover.

Card

  • Card var(--sp-100-s4) — padding, every side; 4 in every SP mode.var(--sp-130-s24) — corner radius; grows to 130 % and holds there.var(--sp-100-s24) — the content's minimum height: the card is 32 high at least.1px — the border, drawn outside. The SP scale has no s1 — a hairline, as on Floating Button.Width — fills the root less its margins (344 at 360). Height — hugs the content.
  • Custom Base --floating-container-padding — re-points the padding.--floating-container-radius — re-points the radius.On L nothing moves; on the custom base each is a default a design request may re-point to another SP token. Overview's second specimen:var(--sp-100-s8) — its padding;var(--sp-130-s28) — its radius.

Scaling

Checkout
100%
Checkout
130%
Checkout
150%
  • Fixed var(--sp-100-s8) — the margin: 8 / 8 / 8.var(--sp-100-s4) — the padding: 4 / 4 / 4.Figma binds SP/100% ratio, so they hold their 100% size in every mode.
  • Capped var(--sp-130-s24) — the radius: 24 / 31.2 / 31.2.Figma binds SP/130% ratio — "s24 / (max*130%)".
  • Content var(--sp-130-s64) — the Main CTA's height: 64 / 83.2 / 83.2, on Button's own contract.The components scale on their own contract and the card hugs them: 72 / 91.2 / 91.2 here.

Overlay and Home Indicator

Both on — the default
Overlay off
Home Indicator off
  • Overlay --background-primary — on by default: transparent to opaque across the root's margin, behind the card.floating-container--no-overlay — removes it; the strip keeps its fill.On a grey screen here, so the fade shows.
  • Home Indicator On by default — where the container meets the screen's bottom edge.floating-container--no-home-indicator — removes the strip, for a container with something below it — a TabBar.var(--sp-100-s8) — where the root then ends, under the card.

RTL

Cancel Order
LTR
إلغاء اطلب
RTL
  • Nothing Mirrors The container is symmetric — nothing in it mirrors. The content follows the reading direction on its own contract: the two buttons swap sides. Figma stages no RTL; the web takes it from dir="rtl".
  • Buttons Stretch Across Two buttons in the card share its width equally — each on Button's Width = fill, the host's row laying them out; no empty space is left at the sides.

Animation

The card presses with the Button it holds. Everything else moves on its own: the content scrolls under the container, and the components inside it keep their own tokens.

Live — press Checkout: the card presses with it. Scroll the list, or Tab through its rows: the container stays, the rows fade under its margin, and a focused row comes up above the card
  • Presses With Its Button --component-push-button-press — the card scales to 95 % with the one Button it holds (pushButton).--component-push-button-release — back to 100 %, in step with the Button.The Button holds still inside the card: one shape presses, rather than a Button shrinking away from the card's edge. A disabled or Skeleton Button presses neither.With two or more controls in the card, each presses on its own and the card stays still.
  • Floats Over Scrolling Content The content scrolls beneath; the container stays. The overlay fades the rows passing under the card's margin; the list's bottom inset — the container's measured height — lets the last row come up above the card, and a row that takes the focus is scrolled into view above it.
  • Re-Hugs, Not Animated A content change re-hugs the card in the same frame — no height tween.
  • Content Moves On Its Own Tokens Each component inside keeps its own motion contract — a Cell's highlight, a TextField's focus. The press of a lone Button is the one the card takes over.
  • Shown Or Hidden On A Screen It appears and leaves with its screen, on the screen's transition. A container that appears or hides 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 --component-push-button-press / --component-push-button-release — zeroed in motion.css under reduced motion: the card's press is instant, as the Button's.No override of its own.

Usage

FloatingContainer holds an action over content that stays in view — a map, a list, the camera, navigation. It is used where a Floating Button cannot be applied.

  • 1
    On The Map The main action over a map that stays in view, in one style with the Floating Buttons on the map. Home Indicator on — the container meets the screen's bottom edge.
  • 2
    Over A List The checkout over a scrolling list: the list scrolls under the card, and its bottom inset — the container's height — lets the last row come up above it.
  • 3
    One Main Action Two equal main actions compete. Keep one; a secondary action beside it is a Secondary Button (Layout · RTL shows the pair).
1
Order
Do: On the map — one main action; the map and its Floating Button stay in view
2
Thai Rice
Ready meal
$6.50
Pasta Carbonara
Ready meal
$8.20
Tuna Sandwich
Breakfast
$4.90
Club Sandwich
Breakfast
$5.40
Fried Potatoes
Sides
$3.10
Breaded Chicken
Ready meal
$7.30
Pay $28.10
Do: Over a list — the checkout stays at hand while the list scrolls under it
3
Order Schedule
Don't: Two main actions — they compete; keep one

Accessibility

FloatingContainer is a container, not a control: assistive technology meets the components inside it, each on its own contract. What the container owes is placement — its reading order, and a focus the host keeps clear of the card.

Checkout
1
2
3
  • 1
    A Group, Not A Control No role, no name, no value, no focus stop. Web: <div>s with no role, no aria-label, no tabindex.TalkBack: no node of its own — Modifier.semantics { isTraversalGroup = true } keeps its components together (Compose 1.5 or later).VoiceOver: accessibilityElement(children: .contain) — a group whose components stay separate elements.The trap: Modifier.semantics(mergeDescendants = true) on Android and children: .combine on iOS fuse the components into one stop — a Button and a TextField, neither reachable on its own. clearAndSetSemantics removes them from the tree altogether, and so does accessibilityHidden(true) on the iOS content wrapper. Hide only the card's background shape.
  • 2
    Decoration Is Silent The overlay — the root's ::before with no text, outside the accessibility tree; it takes no pointer events of its own. The root's box — the margins and the strip — still takes them: what lies under it is reached by scrolling it clear.The card's border and shadow — drawing, not nodes; on iOS the background shape is accessibilityHidden(true).The home-indicator strip — padding; it holds nothing. On the web it is env(safe-area-inset-bottom): present on a device with a home indicator when the page opts into viewport-fit=cover, zero elsewhere.
  • 3
    Components Announce Themselves Each on its own contract — the Main CTA here is announced as that Button. A disabled one is "Disabled" on TalkBack and stays in swipe navigation; "Dimmed" on VoiceOver and stays reachable. Inside the card they are read in layout order — start to end, top to bottom; the start follows the reading direction where a component supports RTL.
  • Reading Order Read together, at the container's place in the screen — after the content for a bottom container; floating over the content does not move it earlier.Web: the markup follows the content it floats over — position: fixed / sticky moves it visually only, the DOM order is the reading and Tab order (WCAG 1.3.2, 2.4.3).TalkBack: where the default positional order would interleave the container with the items under it, the host gives it a traversalIndex higher than the content's default 0f — to verify with TalkBack.VoiceOver: safeAreaInset(edge: .bottom) places it after the content, in the view order and by position. Laid over in a ZStack, it is read after the content when declared after it; declared earlier, the host sets an accessibilitySortPriority lower than the content's (higher is read first).
  • The Focus Stays Clear Of The Card WCAG 2.2 · 2.4.11 Focus Not Obscured (AA) for keyboard focus on the web; the same intent for screen-reader focus on iOS and Android. Two parts, both the host's: a bottom inset as tall as the container — the last item scrolls above the card — and a focused element brought into view above it.Web: padding-bottom and scroll-padding-bottom on the scroll container (html when the document scrolls) do both; the height is measured, not assumed — the Animation demo measures it with a ResizeObserver: Tab through its rows.TalkBack: LazyColumn(contentPadding = …) gives the inset — Scaffold's bottomBar only hands it over as innerPadding. An item wholly under the card can still take the focus without a scroll: the host brings it into view (BringIntoViewRequester, animateScrollToItem), or ends the viewport above the card (a Column with the content weight(1f)) — the dependable form, since Compose exposes no accessibility-focus callback.VoiceOver: safeAreaInset(edge: .bottom) insets the content — a container laid over it another way sets the scroll view's bottom content inset to its measured height. An item under the card can still take the focus without a scroll: the host scrolls it clear when it takes the VoiceOver focus (@AccessibilityFocusState with ScrollViewReader's scrollTo(_:anchor:)), or ends the scroll view above the card.Ending the scroll view above the card costs the overlay's fade-under: nothing scrolls under the card any more.On iOS and Android the screen reader reveals an item against the scroll view's bounds, which do not know about the card — to verify on a device.
  • A TextField, Zoom And Large Text With a TextField inside, the container rides above the on-screen keyboard — Android on the IME inset (Modifier.imePadding()). On iOS Safari a position: fixed container stays on the layout viewport: the host sizes it against visualViewport, or unpins it.Where little of the content stays in view — 400 % zoom (320 CSS px), the largest text sizes, the keyboard open — the host lets the container scroll with the content instead of pinning it: in flow, or sticky inside the content (WCAG 1.4.10 Reflow, 1.4.4 Resize Text).
  • The Press Is Visual Only With one Button inside, the card presses with it — drawn from the Button's own press state. The card gains no handler, role or focus stop: the Button stays the only target, announced on its own contract, and the card does not press while the Button is disabled or a Skeleton.
  • 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; nothing is announced for the container.
  • Text Scaling var(--sp-130-s24) — the corners grow to 130 % and stop.var(--sp-100-s8) — the margins stay; the card hugs its components at every size, and the bottom inset follows the container's measured height.
  • Reduced Motion --component-push-button-press / --component-push-button-release — the card's press, zeroed in motion.css under prefers-reduced-motion.The card still steps to 95 % and back, instantly, as the Button does; nothing is announced.
  • 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.
  • Contrast --border-transparent — the card's edge, with the M shadow: no 3:1 duty — the card is neither a control nor a graphic needed to understand the content (WCAG 1.4.11).--surface-floating — the card's fill, which the components are read on, whatever the screen's background.--text-and-icon-primary — 17.76:1 Day, 15.52:1 Night on it (WCAG 1.4.3; measured from the primitives).--text-and-icon-secondary — 5.25:1 Day, 7.21:1 Night.--floating-container-fill — a re-pointed fill re-checks every component's pairs, in both themes.