A progress indicator for multi-step flows, in bullet, circular and dash forms. Merges the former Stepper - Bullet, Stepper - Circular and Stepper - Dash.
Keep — all findings resolved
Rebuilt on node 4337:11140 in the 2026 Working File as a single Stepper, merging the former Bullet, Circular and Dash components into Type × Steps × Current × Status. Step count is a property rather than sibling components, the ordinal position enum is now an integer, Status states exist where the originals had almost none, property naming follows the guidelines throughout, and the step markers are vectors. The full 2–10 range, the per-Type Status coverage and horizontal-only orientation are all deliberate. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.
In Context
Stepper - Bullet appears at the top or bottom of multi-step flows — most commonly paginated onboarding, swipeable carousels of tutorial cards, and photo galleries where the user needs a minimal position indicator without numerical labels.
Live Preview
Content (proposed)
steps
current
value2 of 4
DS Health
Reusable
Pass
One component now covers all three visual forms and every step count from 2 to 10, where the same coverage previously took three components and twelve siblings between them.
Self-contained
Pass
Owns its typography, spacing and status colours, and the step markers are vector shapes bound to tokens rather than baked assets. Nothing external required to render.
Consistent
Pass
Type, Steps, Current and Status are orthogonal and correctly named — Type is a §1 standard property, integers replace the old ordinal enum, and every value is Title Cased per §5.
Composable
Pass
Drops into any multi-step flow, and all three visual forms share one API so a screen can switch between them without swapping components. Horizontal-only is a deliberate scope decision.
Behavior
State
iOS
Android
Figma Property
Notes
Current step
Yes
Yes
fill = bg
Dot at current index fills in main/stepper/color/bg (#005CE5).
Other steps
Yes
Yes
fill = bg-track
All other dots fill in main/stepper/color/bg-track (#D2E5FF). Note: the spec does not distinguish completed vs upcoming — both look identical.
Completed vs upcoming
N/A
N/A
Not modeled
Bullet steppers in other systems often shade completed dots differently from upcoming ones. This family collapses both into the track color — direction of travel is lost.
Clickable / interactive
N/A
N/A
Not modeled
Used in carousels, some implementations let the user tap a dot to jump to that page. No pressed / focused state exists today.
Connector line
N/A
N/A
Not modeled
Classic Material / iOS bullet steppers draw a thin line between dots tinted to match completed / upcoming. This family uses blank 8-px gaps instead.
Issues
Code Connect mappings not registered. Blocked — no native library exists yet. The schema maps cleanly once one exists: Type as an enum, Steps and Current as integers, Status as an enum. C7 · Code Connect Linkability
Three Stepper components merged into one. v2.0: Rebuilt on node 4337:11140 in the 2026 Working File as a single Stepper set. Stepper - Bullet, Stepper - Circular and Stepper - Dash are now Type = Bullet | Circular | Dash — the unification all three assessments recommended independently. (C4 · Family) C4 · Native Mappability
Step count is a property, not sibling components. v2.0: Bullet had three siblings and Circular nine, each hard-coding a step count; Dash encoded it as ten propNStepper booleans. All of that is now a single Steps axis. (C2 · Property) C2 · Variant & Property Naming
Ordinal position replaced by an integer. v2.0: The highlighted = 1st | 2nd | … | Nth ordinal enum is now Current carrying integers, which maps directly to a currentStep: Int parameter rather than needing a lookup table. (C2 · Rename) C2 · Variant & Property Naming
Status states added. v2.0: Status = Current | Completed | Upcoming | Error now exists where none of the three originals distinguished completed from upcoming, and only Dash had any notion of error. (C5) C5 · Interaction State Coverage
Style renamed to Type. v2.1: §6 names Style explicitly as a catch-all to avoid, alongside Configuration and Settings. Type is a standard variant property from §1 and says the same thing without the anti-pattern. (C2 · Rename) C2 · Variant & Property Naming
All variant values Title Cased. v2.1: Bullet | Circular | Dash and Current | Completed | Upcoming | Error now follow §5, replacing the lowercase values. (C2 · Rename) C2 · Variant & Property Naming
Uneven Status coverage confirmed intentional. v2.1: Closed by owner decision — Bullet carries only Current and Completed, Circular is almost entirely Current with single Completed and Upcoming entries, and Dash carries Error throughout. Each Type supports the statuses its usage actually calls for rather than filling out a uniform matrix. (C5) C5 · Interaction State Coverage
Full 2–10 step range retained. v2.1: Closed by owner decision, superseding an earlier intent to cap the range — the set keeps all 225 variants. Figma cannot parameterise a repeating count, so enumerating every combination is the only way to make each one selectable; capping would mean any flow longer than the cap had no variant and a designer would have to detach. The cost is set size in Figma only — the native API takes steps and current as integers and has no such limit. (C2) C2 · Variant & Property Naming
Dots and ring arcs confirmed as vectors. v2.1: Confirmed by the component owner — the step markers are vector shapes, not the raster PNGs all three original assessments flagged. They recolour with tokens and stay crisp at any density. Not independently verifiable from the assessment tooling, whose response limit the node exceeds. (C6 · Asset) C6 · Asset & Icon Quality
Horizontal-only orientation confirmed intentional. v2.1: Closed by owner decision — Stepper ships horizontal only. An Orientation axis would multiply an already large set for a layout the product does not use, and vertical progress indication is better served by a different pattern than a rotated stepper. Revisit only if a long-flow screen genuinely calls for it. (Composition) C4 · Native Mappability
Recommendations
Announce "Step X of Y" to screen readers. All three original assessments raised this and it is still unaddressed. A progress indicator that conveys position only visually is invisible to assistive technology — the native component should expose the position as an accessibility value. A11y
Styles
Stepper - Bullet
DESDEV
Horizontal row of N 8×8 dots with one dot filled in brand blue to indicate the current step. 3 hardcoded sibling frames today; target is one component with a <code>steps</code> prop.
Dot-style step indicator. Active dots are brand-blue; inactive dots are pale-blue.
Role
Token
Default
Active dot
stepper/color/bg
#005CE5
Inactive dot
stepper/color/bg-track
#D2E5FF
Property Mapping
Figma Property
SwiftUI
Compose
Stepper - Bullet - N Steps (×3 siblings)
Stepper - Bullet (single component)
EBStepperBullet
(implicit in sibling name)
steps: 3…10
total: Int
highlighted = 1st | … | Nth (inner symbol)
current: 1…steps
current: Int
(horizontal only)
orientation: horizontal | vertical
orientation: Axis = .horizontal
(single track color for all non-current)
status: completed | current | upcoming (per slot)
(derived internally from current)
(raster dot PNGs)
Vector Ellipse fills
Circle().fill(...)
Accessibility
Requirement
iOS
Android
Progress role
Wrap the row in .accessibilityElement(children: .ignore) and expose one semantic label. Use .accessibilityValue("Step \(current) of \(total)").
Merge descendants via Modifier.semantics(mergeDescendants = true) with contentDescription = "Step $current of $total".
Value announcement
VoiceOver reads "Step 2 of 4". Do not announce each dot individually — they're decorative at the unit level.
TalkBack reads the merged label. Update stateDescription when current changes to trigger re-announcement.
Touch targets (if interactive)
Dots are 8×8 — far below the 44×44 HIG minimum. If tappable, wrap each in a Button with an 18-px invisible hit-area padding on all sides.
Same. Wrap each dot in Modifier.clickable().minimumInteractiveComponentSize() so the 48-dp target is enforced.
Contrast
Active #005CE5 on white = 5.3:1 ✓. Inactive #D2E5FF on white = 1.2:1 — below the 3:1 non-text graphic threshold. Inactive dots rely on position + count to communicate, not contrast alone. Pair with announced label.
Same ratios. Same concern.
Reduce motion
If animating between current values, honor UIAccessibility.isReduceMotionEnabled and snap instead of cross-fading.
Honor Settings.Global.ANIMATOR_DURATION_SCALE and skip the animation when disabled.
Criteria Scorecard
ID
Criterion
Status
Notes
C1
Layer Structure & Naming
Requires Rework
Step count is modeled as 3 sibling components (Stepper - Bullet - 3/4/5 Steps) instead of a property. Collapse to a single Stepper - Bullet with steps + current.
C2
Variant & Property Naming
Requires Rework
Nested highlighted = 1st | 2nd | … | Nth ordinal axis should become a top-level integer current: 1..steps. Ordinals don't compose across step counts.
C3
Token Coverage
Ready
Fills bound to main/stepper/color/bg and main/stepper/color/bg-track. Padding bound to space/space-4, gap to space/space-0. Shared token set with Stepper - Circular and Stepper - Dash.
C4
Native Mappability
Requires Rework
No native primitive matches. Requires custom EBStepperBullet on both platforms. Unify with Dash + Circular under a shared EBStepper(style:) API.
C5
Interaction State Coverage
Requires Rework
No completed / upcoming differentiation, no pressed / focused / tappable state, no connector line, no vertical orientation. Every non-current dot is identical.
C6
Asset & Icon Quality
Requires Rework
Dots are raster <img> PNGs — two per sibling (filled + track). Replace with vector Ellipse with token-bound fills. Trivial vector; no reason to bake as raster.
C7
Code Connect Linkability
Not Mapped
Blocked until 3 siblings collapse to one component and raster dots are replaced with vectors. Consider mapping through a unified EBStepper(style: .bullet) API.
Variants Inventory (0 total)
Today: 3 sibling components, one per hardcoded step count, each with Nhighlighted variants (3+4+5 = 12 pre-baked variants). Target: 1 component with steps: 3…10 and current: 1…steps as runtime properties; no pre-baked variants needed.
Verdict: Restructure — Collapse 3 sibling components (Stepper - Bullet - 3/4/5 Steps) into one Stepper - Bullet with steps: Int and current: Int properties. Replace raster dot PNGs with vector ellipses. Long-term, unify with Dash + Circular under EBStepper(style:). Open
Schema
C1 — Family structure — 3 top-level components differ only by hardcoded step count. Collapse into one component with a steps property. Same anti-pattern as Stepper - Circular. Open
C1
C2 — Property shape — Nested highlighted = 1st | 2nd | … | Nth ordinal axis should become a top-level integer current. Open
C2
C4 — Native mapping — No native primitive matches. Requires custom EBStepperBullet on both platforms built over HStack/Row of Circle shapes. Consider unifying with Dash + Circular under EBStepper(style:). Open
C4
C5 — Missing states — No completed / upcoming distinction, no pressed / focused states for interactive carousels, no connector line, no vertical orientation. Open
C5
C6 — Raster dots — Each 8×8 dot is a pre-baked PNG. Replace with vector Ellipse fills bound to main/stepper/color/bg and main/stepper/color/bg-track. Open
C6
C7 — Code Connect — Mappings pending restructure. Mapping 3 separate siblings would codify the anti-pattern. Open