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.
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
States
One state — Standard. FloatingContainer is not interactive.
Standard
-
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
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
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-overlayturns 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-indicatorturns 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 setsviewport-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
-
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
-
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
-
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.
-
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).
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.
-
1
A Group, Not A Control No role, no name, no value, no focus stop. Web:
<div>s with norole, noaria-label, notabindex.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 andchildren: .combineon iOS fuse the components into one stop — a Button and a TextField, neither reachable on its own.clearAndSetSemanticsremoves them from the tree altogether, and so doesaccessibilityHidden(true)on the iOS content wrapper. Hide only the card's background shape. -
2
Decoration Is Silent The overlay — the root's
::beforewith 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 isaccessibilityHidden(true).The home-indicator strip — padding; it holds nothing. On the web it isenv(safe-area-inset-bottom): present on a device with a home indicator when the page opts intoviewport-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/stickymoves 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 atraversalIndexhigher than the content's default0f— to verify with TalkBack.VoiceOver:safeAreaInset(edge: .bottom)places it after the content, in the view order and by position. Laid over in aZStack, it is read after the content when declared after it; declared earlier, the host sets anaccessibilitySortPrioritylower 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-bottomandscroll-padding-bottomon the scroll container (htmlwhen the document scrolls) do both; the height is measured, not assumed — the Animation demo measures it with aResizeObserver: Tab through its rows.TalkBack:LazyColumn(contentPadding = …)gives the inset —Scaffold'sbottomBaronly hands it over asinnerPadding. 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 (aColumnwith the contentweight(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 (@AccessibilityFocusStatewithScrollViewReader'sscrollTo(_: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 aposition: fixedcontainer stays on the layout viewport: the host sizes it againstvisualViewport, 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, orstickyinside 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 aCanvasTextoutline 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.