v3.0.0
ProgressButton

A timer button: a Button preset with a progress line over its fill. The line runs for a set time — 15 seconds by default — and the screen moves on when it ends.

3.0.0 -- -- 3.1.3
Accept

Overview

ProgressButton is an action with a deadline — accept an offer, confirm a driver, keep a search running. The time left is a line that moves across the button; when it ends, the screen decides what happens next. It is built on Button: the same four configurations, slots, rows and 3.4 scaling, plus the line. Shown frozen six seconds into a 15-second run.

  • 1
    Main CTA
  • 2
    L
  • 3
    M
  • 4
    S
  • 5
    customProgressButton
1
Accept
2
Accept
3
Accept
4
Accept
5
AcceptCourier

States

Pressed is Button's press — a 95% scale, no overlay (Animation). Loading keeps the line and stops it where it was. Disabled and Skeleton carry no line and are the same on every size; Disabled is the usual state once the time is up. Loading and Skeleton take Button's fixed widths — var(--sp-s72) on L and M, var(--sp-s64) on S.

  • 1
    Standard Fill var(--background-brand) (--progress-button-fill); label, icons and Loader var(--text-and-icon-always-dark); track var(--surface-dark-overlay), edge band var(--surface-white-overlay).
  • 2
    Loading Standard's colours; the Loader replaces the label and slots, and the line pauses.
  • 3
    Disabled Fill var(--surface-on-white), label and icons var(--text-and-icon-disabled); no line. A token swap, never opacity.
  • 4
    Skeleton The Skeleton component — var(--skeleton-on-white); no line.
Main CTALMS Standard
Accept
Accept Accept Accept Loading
Disabled
Accept
Accept Accept Accept Skeleton

The line over a Countdown

1
Accept0 s
2
Accept5 s
3
Accept10 s
4
Accept15 s
  • 1
    Uncovered The run starts with the brand fill whole — Figma's full frame.
  • 2
    Running The track grows from the end side. Its cover is var(--surface-dark-overlay), its slanted edge carries a var(--sp-s2) band in var(--surface-white-overlay).
  • 3
    Two-thirds Figma's five line frames — full, start, middle, end, empty — are points of one movement; the web draws every point between them.
  • 4
    Time is up Fully covered — Figma's empty. The button stays as it is and the screen applies the next step.

Anatomy

The root carries the Button classes — .button .button--{size} — plus .progress-button. Everything but the line is Button's.

1
Accept
2
Accept
3
Accept
  • 1
    Root .progress-button — the focusable, clickable element. Clips the line to its radius.
  • 2
    ProgressLine .progress-button__line — an absolute layer behind the content, holding the .progress-button__track. Hidden from assistive technology.
  • 3
    StartSlot · StartText · EndSlot Button's slots and rows, above the line. Both slots are off by default and absent on Main CTA.

Layout

Width hugs the content; height has a minimum per size and grows only with wrapped text (Button 3.4). The redlines are drawn from the rendered button, at 100%. Each value takes the SP family Figma binds: var(--sp-130-sN) for height, radius and vertical padding (they stop at 130%), var(--sp-100-sN) for horizontal padding and the gaps (they hold at 100%).

customProgressButton

AcceptCourier
customProgressButton — both slots and both rows on
  • General The base — slots, rows and fill are open. Unlike customButton it keeps the preset spacing: s12 padding and s4 gap, not s16 and s12.var(--sp-130-s56) — minimum height, cap130: 56 / 72.8 / 72.8. The same as L; a wrapped label grows it.var(--sp-130-s8) — vertical padding, cap130: 8 / 10.4 / 10.4. What stays above and below a wrapped label.var(--sp-100-s12) — padding on the root, fixed, both sides. A side ends at 16 or 20 — see L, 3.var(--sp-100-s4) — gap, fixed, between StartSlot / StartText / EndSlot.var(--sp-130-s20) — roundness, cap130: 20 / 26 / 26.Fill --progress-button-fill — var(--background-brand) by default; the one thing the base recolours.Hug — by default. Centre / centre — the content on both axes.
  • 1
    StartSlot / EndSlot On by default — both (Figma Slot#1 / Slot#2). The presets turn them off.var(--sp-s24) — the IconContainer inside. Hug — the slot takes its size.var(--sp-100-s4) — outer padding, fixed, toward the button edge.
  • 2
    TextContainer var(--sp-100-s8) — horizontal padding, fixed; 0 vertical.var(--sp-100-s2) — gap, fixed, between Title and Subtitle.Heading 4 and Compact Body, both rows on by default.
  • 3
    ProgressLine Fills the root edge to edge, behind the content — no padding of its own. Figma Progress Line boolean, on by default; it clips to the root's radius.

Main CTA

Accept
Main CTA — filling its container
  • General Locked, all of it. A shape these numbers do not cover is built from customProgressButton.var(--sp-130-s64) — height, fixed, cap130: 64 / 83.2 / 83.2. It does not grow with content, even with multiline on.var(--sp-130-s8) — vertical padding, cap130: 8 / 10.4 / 10.4. Button's Main CTA has none.var(--sp-100-s16) — padding, fixed, the same on both sides. One number, since nothing stands beside the label.var(--sp-100-s12) — gap, fixed, bound in Figma but with nothing to separate: Main CTA holds one row.var(--sp-130-s20) — roundness, cap130: 20 / 26 / 26.Fill — by default; changeable to hug. Centre / centre.
  • 1
    TextContainer Heading 2 — PP Agrandir, cap130. One row, no subtitle.One line, truncating to an ellipsis. Multiline wraps it to two lines at most.Loader var(--sp-100-s32) — a fixed 32 in Loading.

L

AcceptCourier
L — both slots on
Accept
L — StartSlot only
  • General Locked, all of it. Height, padding, gap, roundness and both text styles are fixed by the preset.var(--sp-130-s56) — minimum height, cap130: 56 / 72.8 / 72.8. A wrapped label grows it.var(--sp-130-s8) — vertical padding, cap130: 8 / 10.4 / 10.4.var(--sp-100-s12) — padding on the root, fixed. What a side ends up at is 16 or 20 — see 3.var(--sp-100-s4) — gap, fixed, between StartSlot / StartText / EndSlot.var(--sp-130-s20) — roundness, cap130: 20 / 26 / 26.Hug — by default; changeable to fill. Centre / centre.
  • 1
    StartSlot / EndSlot Off by default — both.var(--sp-s24) — the IconContainer inside, full scaling.var(--sp-100-s4) — outer padding, fixed, toward the button edge.
  • 2
    TextContainer var(--sp-100-s8) — horizontal padding, fixed; 0 vertical.var(--sp-100-s2) — gap, fixed, between Title and Subtitle.Heading 4 and Compact Body. Subtitle off by default; one line each, multiline opt-in per row.Loader var(--sp-130-s24) in Loading, cap130.
  • 3
    Padding by what the side ends in 16 — a side that ends in a slot. var(--sp-100-s12) on the root plus var(--sp-100-s4) on the slot — both fixed, so the 16 holds in every mode. An icon carries its own air.20 — a side that ends in a label. var(--sp-100-s12) on the root plus var(--sp-100-s8) on StartText. Text runs to its own edge, so it needs more.The same on M and S — the paddings are Button's.

M

Accept
M — both slots on, one row
  • General Locked, all of it.var(--sp-130-s48) — minimum height, cap130: 48 / 62.4 / 62.4.var(--sp-130-s8) — vertical padding, cap130: 8 / 10.4 / 10.4. On the root — Button's M has none there and puts a fixed 4 on StartText instead. So with the subtitle on, M grows past its minimum: 20 + 2 + 16 + 2 × 8 = 54 at 100% (Button's M stays at 48). The text scales with SP, so the two rows read 54 / 69.6 / 76.8 at 100 / 130 / 150%.var(--sp-100-s12) — padding on the root, fixed; var(--sp-100-s4) — gap, both fixed.var(--sp-130-s20) — roundness, cap130: 20 / 26 / 26. Where Button holds M at a fixed 20.Hug — by default. Centre / centre.
  • 1
    StartSlot / EndSlot Off by default — both.var(--sp-130-s20) — the IconContainer inside, cap130.var(--sp-100-s4) — outer padding, fixed, toward the button edge.
  • 2
    TextContainer var(--sp-100-s8) — horizontal padding, fixed; 0 vertical.var(--sp-100-s2) — gap, fixed, between Title and Subtitle.Main Body and Compact Body.Loader var(--sp-130-s16) in Loading, cap130.

S

Accept
S — both slots on, one row
  • General Locked, all of it.var(--sp-130-s40) — minimum height, cap130: 40 / 52 / 52. Below the 48 target at 100% — for dense contexts.var(--sp-130-s8) — vertical padding, cap130: 8 / 10.4 / 10.4. With the subtitle on, S grows past its minimum: 20 + 2 + 16 + 2 × 8 = 54 at 100%; the text scales with SP, so the two rows read 54 / 69.6 / 76.8 at 100 / 130 / 150%.var(--sp-100-s12) — padding on the root, fixed; var(--sp-100-s4) — gap, both fixed.var(--sp-130-s20) — roundness, cap130: 20 / 26 / 26. At 40 tall it reads as a pill.Hug — by default. Centre / centre.
  • 1
    StartSlot / EndSlot Off by default — both.var(--sp-s16) — the IconContainer inside, full scaling.var(--sp-100-s4) — outer padding, fixed, toward the button edge.
  • 2
    TextContainer var(--sp-100-s8) — horizontal padding, fixed; 0 vertical.var(--sp-100-s2) — gap, fixed, between Title and Subtitle.Main Body and Caption — the subtitle drops to the 12 floor.Loader var(--sp-130-s16) in Loading, cap130.

Scaling — L / M / S

1
AcceptAcceptAccept
2
AcceptAcceptAccept
3
AcceptAcceptAccept
  • 1
    100% Minimum height var(--sp-130-s56) / var(--sp-130-s48) / var(--sp-130-s40), radius var(--sp-130-s20), vertical padding var(--sp-130-s8) — 56 / 48 / 40, 20, 8.
  • 2
    130% 72.8 / 62.4 / 52, radius 26, vertical padding 10.4 — the family's ceiling. M and S cap their radius here too, where Button holds a fixed 20. Horizontal padding var(--sp-100-s12) and gap var(--sp-100-s4) stay 12 and 4.
  • 3
    150% The same as 130% — var(--sp-130-sN) stops there. The Loader too (var(--sp-130-s24), var(--sp-130-s16)); Main CTA's is var(--sp-100-s32), a fixed 32.

The slanted edge

1
Accept
2
Accept the ride request
  • 1
    64°, at every height A taller button gets a longer edge, not a steeper one. The line is its own size container, so the track measures the button in cqh and keeps the angle. Not customisable.
  • 2
    The band var(--sp-s2) of var(--surface-white-overlay) on the edge only. Both overlays are alpha tokens, so they read on any fill — the brand fill here, a custom one on customProgressButton via --progress-button-fill.

RTL

1
Accept
2
قبول
  • 1
    LTR The track grows from the right.
  • 2
    RTL Progress is mirrored: the track grows from the left and its edge leans the other way. Set dir="rtl" on the root or an ancestor.

Animation

The line is a timer, not a transition. Shown here as a three-second run on a loop; the default is 15, and a real run plays once.

The run

1
Accept
2
Accept
  • 1
    Countdown — default Starts uncovered; the track grows until it covers the button. calc(var(--progress-button-time) * 1s) on var(--curve-linear) — Figma's linear, 0.25 / 0.25 / 0.75 / 0.75. The primitive is consumed directly, a recorded exception: no DS preset covers a run measured in seconds.
  • 2
    Countup .progress-button--countup — the same run backwards: starts covered, ends with the fill whole.
  • Compositor only The track moves by transform alone, never resizing a box. It fires animationend when the time is up; the screen listens and applies the next step.
  • Reduced motion The line is kept — it is the time left — and moves one step per second: steps(var(--progress-button-time), end).

Press and Loading

1
Accept
2
Accept
3
  • 1
    Rest 100%.
  • 2
    Press — pushButton var(--component-push-button-press), 100% → 95%, as Button. No overlay: the Specification frame's 50% PressedOverlay is not built (owner, 2026-11-04).
  • 3
    Loading The Loader spins on var(--pattern-spin); the line pauses where it was.

Usage

Use ProgressButton for an offer or a choice that expires on its own, or for a step that continues by itself unless the person acts. An action with no deadline is a Button; progress of a task the person waits for (an upload, a search) is a progress indicator — the time is not theirs to use.

  • 1
    One timer per screen Its alternative, such as Decline, is a plain Button beside it.
  • 2
    No second timer Two running lines ask a person to track two deadlines at once.
  • 3
    Time to read 15 seconds by default; keep the button on screen for at least 15–20 seconds so a person can read what they are accepting. Below that the line pressures rather than informs.
1
Decline Accept
✓ One timer; the alternative is a plain Button
2
Decline Accept
✕ Two timers — two deadlines at once

Accessibility

ProgressButton inherits Button's whole contract — role, Loading name, slots, focus, keyboard, target size. The specimen is a real <button>: Start runs a shortened, six-second run — the frame's timeline from its final five seconds (a 15-second run adds the 10-second announcement) — and the caption under it is the live region — shown here on purpose; in a product it carries .visually-hidden. The per-platform tables live in specs/components/progress-button/progress-button-a11y.md.

Screen reader

1
6 seconds remaining Press Start to run the timeline
  • 1
    One Button, the Time as Its Description TalkBack reads the time inside the description — contentDescription: [Label] [Subtitle] [Time]. "Button. Accept, Courier. 15 seconds remaining. Double tap to activate."VoiceOver reads it as a value, with the traits Button and Updates Frequently. The web's equivalent: aria-describedby on a visually hidden "N seconds remaining", updated every second.The line is aria-hidden="true". It is a picture of the time, not a second source of it.
  • The Announcement Timeline 15 s on focus, 10 s ("10 seconds remaining, 66 percent"), 5 s ("Final 5 seconds"), then 4, 3, 2, 1, and 0 s ("Time is up, Button disabled").Spoken by a aria-live="polite" region outside the button. Text inside a <button> becomes its name — a region inside would rename and re-announce it every second.
  • Edge States Time is up: TalkBack "[Label], Button, Disabled"; VoiceOver appends "Dimmed" (notEnabled). The time leaves the description on TalkBack and the value is cleared on VoiceOver. Focus stays on the button — aria-disabled, not disabled.Loading: TalkBack "[Label], Button, Busy"; VoiceOver keeps the label and exposes busy. The line pauses; the host empties the time text and the live region stays silent.Skeleton: not exposed on either platform.
  • Timing WCAG 2.2.1 Timing Adjustable. The 15-second default is a usability minimum, not a 2.2.1 mechanism. The screen turns the limit off, adjusts or extends it — or records the exception it relies on: essential (a driver's offer expires because the driver moves on) or real-time.Reduced motion keeps the line, one step per second; the announcements are the same.The line is 1.58:1 against the uncovered fill — below the 3:1 of WCAG 1.4.11. It passes only beside the seconds shown as text, in the subtitle row — so a screen where the deadline matters prints the seconds; the gap is accepted (owner, 2026-11-12).