v3.3.0
Floating Button

An action that floats above the content it acts on, and opens from an icon-only square into a labelled button without becoming a different control.

3.0.0 -- -- --
Recentre

Overview

Floating Button carries an action that belongs to the screen rather than to a place in it: recentre the map, start a new message, add an item. It sits above the content on its own elevation instead of in the flow, which is what makes it reachable while the content underneath keeps scrolling.

  • 1
    L · Squircle
  • 2
    L · Button
  • 3
    M · Squircle
  • 4
    M · Button
1
Recentre
2
3
Recentre
4

Styles and States

Component × Style

The same instance is either a Squircle — an icon alone in a square — or a Button, the square opened out to carry a label beside that icon. Height, radius, elevation and border hold across the change; only the width and the label arrive or leave. The Style axis sets the fill and the content colour, and the two axes are independent.

PrimaryErrorInverse L · Squircle L · Button Recentre Recentre Recentre M · Squircle M · Button Recentre Recentre Recentre

States

The State axis across the same three styles, shown at L · Button.

PrimaryErrorInverse Standard Recentre Recentre Recentre Disabled Recentre Recentre Recentre Skeleton

Primary · Standard

The base the L and M presets are locked configurations of.

Recentre
1
2
2
3
4
  • 1
    Fill var(--surface-floating)
  • 2
    Icon and label var(--text-and-icon-primary) — one token for both, which is why the pointer lands twice.
  • 3
    Border — every Style and state var(--border-transparent), 1. It holds the control's edge against a surface of any colour — the fallback where the shadow disappears into a dark background.
  • 4
    Elevation — every Style and state var(--shadow-s). With the border, it is what separates a floating control from whatever surface is behind it.

Error · Standard

Delete
1
2
2
  • 1
    Fill var(--surface-floating) — the same floating surface Primary uses.
  • 2
    Icon and label var(--text-and-icon-error) — the destructive meaning lives in the content, so the action reads as a tinted icon or label rather than a red block.

Inverse · Standard

Recentre
1
2
2
  • 1
    Fill var(--background-inverse-primary) — the one style that changes the surface.
  • 2
    Icon and label var(--text-and-icon-inverse-primary)

Custom · Standard

Recentre
1
2
3
  • 1
    Fill var(--surface-floating) — a default, not a lock: any colour token in the DS can replace it.
  • 2
    Icon and label var(--text-and-icon-primary) — a default, not a lock: any colour token in the DS can replace it.
  • 3
    Title Heading 4 — a default: any DS text style can replace it. The presets lock all of this: an L or M instance changes its slots and its label, never these tokens.

Common · Disabled

Recentre
1
2
2
  • 1
    Fill var(--surface-floating)
  • 2
    Icon and label var(--text-and-icon-disabled) — a token swap, never opacity. Every style collapses here: Inverse loses its dark fill as well, so a disabled Inverse is a light control with disabled content, not a dark one.

Common · Skeleton

1
2
  • 1
    Fill var(--skeleton-on-white) Collapsed the placeholder is the square; opened it takes a fixed var(--sp-s120) width at L rather than a hug.
  • 2
    Wave var(--skeleton-wave)

Anatomy

The parts follow the DS-wide StartSlot / StartText / EndSlot semantics, the same as Button — by construction: the component began as a customButton configuration, promoted into a component of its own. The specimen is the open form with everything on.

Recentre 2 stops ahead
1
2
3
4
5
6
  • 1
    StartSlot .floating-button__start-slot — swap slot, default IconContainer s24. Off by default in Component=Button; the whole content in Component=Squircle.
  • 2
    StartText .floating-button__start-text — the label column, s2 row gap, s8 horizontal padding. Absent in Component=Squircle.
  • 3
    Title row .floating-button__title-row — Heading 4. Required inside StartText.
  • 4
    Subtitle row .floating-button__subtitle-row — optional, the second row of the column.
  • 5
    EndSlot .floating-button__end-slot — mirrors StartSlot.
  • 6
    Indicator .floating-button__indicator — an overlay the size of the root, pinning a s12 Indicator to the top-right corner. Off by default.

Layout

Both forms redlined at L, with M beside them where the height is the only change.

Component = Squircle

L — Squircle
1
M — Squircle
  • General var(--sp-s56) / var(--sp-s48) — the square, L / M. The width is the height.var(--sp-s20) — roundness, at both sizes and in both forms. L collapsed is Squircle L exactly; M collapsed is not Squircle M, which is 48 at 16.var(--sp-s0) — padding, both axes. The square comes from the size, not from padding.1 — the border, every style and state. The hairline has no SP token: the scale runs s0 → s2.
  • 1
    Icon var(--sp-s24) — the IconContainer, centred by geometry. The 16 inset on each side is centring, not padding.

Component = Button

Recentre L — both slots on
StartSlot
1
Recentre 2 stops ahead
StartText
2
Recentre L — StartSlot only
3
  • General var(--sp-s12) — padding on the root. What a side ends up at is 16 or 20 — see 3.var(--sp-s4) — gap between StartSlot / StartText / EndSlot.Hug — the width follows the label; height, roundness, border and elevation are the square's.Centre / centre — the content inside the control, on both axes. Vertical padding is var(--sp-s0).
  • 1
    StartSlot / EndSlot var(--sp-s24) — the IconContainer inside. Hug — the slot takes its size.var(--sp-s4) — outer padding, toward the control edge.Off by default — both. The opened form is an icon and a label only when StartSlot is switched on.
  • 2
    StartText var(--sp-s8) — horizontal padding. It is what makes a label side wider than a slot side.var(--sp-s2) — gap between Title and Subtitle.One line each, truncating to an ellipsis.
  • 3
    Padding by what the side ends in var(--sp-s16) — a side that ends in a slot. s12 on the root plus s4 on the slot.var(--sp-s20) — a side that ends in a label. s12 on the root plus s8 on StartText — the same optical balance as the Button presets (button.md §8).

Animation

Press

Rest — 100%. Hold it. Recentre Held — 95%
1
2
  • pushButton The same press Button uses. Named in motion-rules.md §6.1.Here only the scale moves — no style defines a pressed fill, so nothing else is in the transition.Press is the whole of the pressed state. [FloatingButton] 3.3 has no Pressed member on the State axis — the scale is what a person sees.The scale is from the centre, so the press is unaffected by RTL.
  • 1
    Press var(--component-push-button-press) — 200ms on standard-ease-in-out.transform 100% → 95% — the scale alone.
  • 2
    Release var(--component-push-button-release) — the same 200ms back.transform 95% → 100%. Symmetric by design: a slower release would read as lag.

Squircle ↔ Button

Squircle ↔ Button — tap it
1
  • 1
    Opening var(--transforming-state-slow) — 300ms on standard-ease-in-out: the control's width, the label's width and opacity, all on one token — which is why the hug width equals the sum of its parts at every frame.One catalogued step slower than a state flip. The stretch travels real distance, so it takes State/Slow rather than State/Default — same curve, 300 over 200.Opening is a layout change, and that is deliberate. A control that grows cannot stay on the compositor without distorting its own label; the timing is the catalogued token rather than an invented duration.One markup serves both forms. Closed, StartText stays mounted, collapsed to zero width — which is also what keeps the icon centred in the square.Width interpolates to hug via interpolate-size: allow-keywords; a browser without it snaps between the forms.
  • Reduced motion Zeroed at the token in motion.css, so the component carries no override of its own: the press becomes an instant scale-less tap and the control opens without travelling.

Usage

Floating Button carries an action that belongs to the whole screen and has to stay reachable while the content scrolls: recentre the map, start a new message, add an item. A control that sits in the flow of a screen is Button, whatever its shape.

  • 1
    The screen's action An action a person needs from any position in a long list or on a map. Error for a destructive floating action; Inverse over imagery or a dark surface.
  • 2
    One per screen Two floating actions compete for the same corner and neither reads as the screen's action. The second one belongs in the flow — as a Button.
  • 3
    Opens once, then gets out of the way The Component axis is for a control that has something to say once — on arrival, or the first time a person reaches the screen. A Floating Button that stays open permanently is a Button in the wrong place; the single screen-level action pinned to the bottom of a flow is Button's Main CTA, full-bleed and part of the layout.
1
✓ One action, floating over the screen
2
✕ Two floating actions — neither reads as the screen's
3
Recentre
✕ Open for good, standing in for the CTA — that is Button's job

Accessibility

The three specimens are real <button> elements — a native button carries the role, the focus behaviour and both activation keys on its own. Tab into the canvas to see the ring. TalkBack and VoiceOver agree on the Standard announcement — the label, then Button — and part company on Disabled: TalkBack appends 'Disabled', drops the hint and skips the control in swipe navigation; VoiceOver appends 'Dimmed' and keeps it in the rotor, so the state stays discoverable. The per-platform label, trait and hint tables live in specs/components/floating-button/floating-button-a11y.md.

Screen reader and keyboard

Squircle — the name lives in aria-label; tab to it Button — the same name, the same element Disabled — announced, still findable
1
2
3
  • 1
    Role and Name type="button" on the root. A bare <button> in a form defaults to submit, so every activation would post the form.Collapsed there is no visible label, so the name comes from aria-label.The root is the focusable element, and the only one. No slot, row or overlay takes focus — one stop per control.
  • 2
    Opening Does Not Change the Name Opened, the visible label says the same thing the aria-label said. A person who focused the square and hears it again opened is still on one action, not what sounds like a new one. Where the two must differ, the aria-label is the one that stays.aria-expanded is deliberately absent. It would announce a disclosure and invite a person to look for the region it opened — there is none. The Component axis is presentation.Focus is retained on the same element throughout the change.
  • 3
    Disabled aria-disabled="true" keeps the control in the tab order; the hard disabled attribute removes it. The choice is whether the action should stay findable while unavailable.TalkBack: "[Label], Button, Disabled" — the hint is dropped, and the control is skipped in swipe navigation.VoiceOver: "Dimmed" — and the control stays in the rotor.The keys go quiet in the handler, not the markup. aria-disabled keeps a native button fully clickable — the activation handler guards it, the same "if aria-disabled, return" Checkbox and RadioButton carry.
  • Keyboard Enter and Space both activate — the platform behaviour of a native button.Opening is not a keyboard action of its own. The product decides when the control opens; a person acts on it in either form with the same two keys.:focus-visible — var(--sp-s2) inset in var(--text-and-icon-primary), radius var(--sp-s20). Inset so the ring stays inside the control rather than on the elevation, where the shadow would compete with it.Inverse rings in var(--text-and-icon-inverse-primary). The default ring is the same primitive as the inverse fill in Dark — both white — and a neighbouring grey in Light: invisible either way. The style's content colour is the one token guaranteed to contrast with its fill.
  • Placement and Target The tab order follows the DOM. The markup goes where the action belongs in reading order — commonly right after the region it acts on, not at the end of the document where the elevation might suggest.L var(--sp-s56) is above the 48 the DS uses; M var(--sp-s48) meets it. Collapsed, the width equals the height, so both sizes clear the minimum on both axes. The area under a floating control cannot hold another target — that spacing belongs to the screen.
  • Skeleton and the Indicator Skeleton is removed from the accessibility tree entirely. A person is not offered a control that does not exist yet.The Indicator is exposed only when it says something the label does not. A dot repeating a count already spoken is aria-hidden.