RestructureRequires Rework
Stepper - Bullet Component link

A row of dot markers indicating progress through a multi-step flow, with the current step using a larger emphasis dot.

Restructure — collapse 3 sibling components into one <code>Stepper - Bullet</code> with <code>steps</code> and <code>current</code> properties
Step count is a scalar, not a component axis. The current schema has 3 top-level components (3/4/5 steps) and inside each, an highlighted = 1st … Nth ordinal enum for the active dot. Rebuild as a single component: steps: Int (3–10) and current: Int (1..steps). Replace the raster dot PNGs with a vector Ellipse whose fill is bound to main/stepper/color/bg (active) or main/stepper/color/bg-track (inactive). Native side maps to a custom EBStepperBullet(current:total:) rendered as an HStack / Row of Circle shapes. Better still: unify Dash + Bullet + Circular into one EBStepper(current:total:style: .bullet | .dash | .circular) API.
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
Partial
Fits any low-information position indicator (carousel, onboarding), but the 3-sibling split forces consumers to swap components when step count changes instead of flipping a prop.
Self-contained
Warn
Each 8×8 dot renders as a raster <img>. Two PNG assets per sibling (filled + track) — six total for what should be one vector Ellipse with two token-bound fills.
Consistent
Fail
Step count modeled as 3 top-level components, same anti-pattern as Stepper - Circular (9 siblings). Every other scalar axis in the DS is a property. Breaks the naming hierarchy — "Stepper - Bullet" is three components, not one.
Composable
Partial
Small footprint (8px dots, 8px gaps) composes cleanly into carousel, modal, and onboarding layouts. But no connector line between dots means it reads as isolated markers rather than a progress rail.
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.
Open Issues
  • Step count is modeled as 3 sibling components instead of a steps prop. The family ships as Stepper - Bullet - 3 Steps, - 4 Steps, - 5 Steps — three top-level components that differ only by hardcoded count. Same anti-pattern as Stepper - Circular (9 siblings). Should be a single Stepper - Bullet with steps: Int and current: Int — 3× maintenance collapses to 1×, and the component scales to 6, 7, 8+ without new files. C1 · Layer Structure & Naming
  • Variant axis highlighted = 1st | 2nd | … | Nth uses ordinal enums instead of an integer. Each sibling has a nested symbol with highlighted=1st, 2nd, 3rd, 4th, 5th to mark the active dot. Ordinals don't compose — the 4-step sibling can't have a "5th" option, and there's no path to current=N+1. Promote to a top-level integer current: 1..steps. C2 · Variant & Property Naming
  • Every dot is a raster <img> PNG — 8×8 ellipse baked as an image. Two PNGs per sibling (filled + track) × 3 siblings = six raster assets for what is mathematically a filled circle. Blocks theming (can't retint), breaks at @3x, and ships bytes the native renderer doesn't need. An 8-px Circle() / Box(Modifier.clip(CircleShape)) with a token-bound fill is all that's required. C6 · Asset & Icon Quality
  • No native primitive matches — this needs a custom component. SwiftUI has no BulletStepper; Material 3 provides only LinearProgressIndicator and a linear Stepper. Both platforms need a custom EBStepperBullet built from an HStack/Row of Circle/Box shapes. The raster-baked dots make this worse — the dev can't reuse the asset. C4 · Native Mappability
  • No completed / upcoming distinction, no pressed / focused states, no connector line. The spec treats every non-current dot identically. Users can't see direction of travel. Tappable-to-jump behavior (common in carousels) has no pressed state modeled. And the 8-px blank gaps between dots would normally be a connector rail in classic bullet steppers. C5 · Interaction State Coverage
  • Code Connect mappings not registered. Blocked until the family collapses to one component and the raster dots are replaced with vector circles. Mapping 3 separate siblings would codify the anti-pattern into the tooling. C7 · Code Connect Linkability
Design Recommendations
  • Collapse all 3 siblings into one Stepper - Bullet with steps and current properties. Delete Stepper - Bullet - 3 Steps, - 4 Steps, - 5 Steps as separate components. Create one Stepper - Bullet with steps: 3 | 4 | … | 10 and current: 1 | 2 | … | 10. Variant math drops from 3 top-level × 3–5 highlighted = 12 pre-baked variants to 1 component with runtime-computed fills. Native API: EBStepperBullet(current: Int, total: Int). Family
  • Unify Dash + Bullet + Circular under one EBStepper(current:total:style:) API. All three siblings share the same data shape (current + total) and the same token set (main/stepper/color/*). Collapse them into one native component with style: .dash | .bullet | .circular. Figma keeps three component symbols (different visual languages) but they share the same property schema — making migration / swap between styles trivial. Family
  • Rename the nested highlighted = 1st | 2nd | … | Nth ordinal axis to an integer current. Ordinal enums don't scale and conflate position with presentation. Use current: Int at the top level and let each dot compute its own fill from index == current ? bg : bg-track. Property
  • Replace raster dot PNGs with vector Ellipse fills bound to tokens. Each dot is an 8×8 ellipse — the simplest possible vector. Two fills only: main/stepper/color/bg (active) and main/stepper/color/bg-track (inactive). No PNG assets, resolution-independent, theme-able. Asset
  • Add completed vs upcoming differentiation. Optional but common: completed dots use a muted brand tint; upcoming use the track. Model as status: completed | current | upcoming computed per-slot from current. Adds direction-of-travel cue without the user having to count. State
  • Spec a connector line between dots (optional variant). Classic bullet steppers draw a 1–2 px line between each dot pair, tinted to match completed (brand) vs upcoming (track). Today the 8-px blank gap reads as isolated markers. Add showConnector: Bool (default off for carousel-style use, on for wizard-style use). Property
  • Add an orientation property for vertical layouts. Dot steppers sometimes appear as a vertical list in sidebars or long-form onboarding. Add orientation: horizontal | vertical. Property
  • Build as a custom native component. Neither SwiftUI nor Material has a BulletStepper primitive. Ship EBStepperBullet: iOS uses HStack { ForEach(0..<total) { Circle().fill(index == current ? Color.stepperBg : Color.stepperBgTrack).frame(width: 8, height: 8) } }; Android uses Row { repeat(total) { Box(Modifier.size(8.dp).clip(CircleShape).background(if (it == current) EBTokens.stepperBg else EBTokens.stepperBgTrack)) } }. Composition
  • Announce "Step X of Y" to screen readers. The component is decorative by default — assistive tech reads nothing. Wrap in a semantic container that announces "Step \(current) of \(total)" (SwiftUI .accessibilityLabel, Compose Modifier.semantics { contentDescription = … }). Also: minimum touch target is 44×44 — if dots are tappable, wrap each in a padded hit area, don't make the 8-px dot itself the target. A11y
  • Document the canonical composition and retire the sibling names. Update the sticker sheet page to show one Stepper - Bullet with property controls; add a migration note pointing Stepper - Bullet - N Steps consumers at the new steps prop. Docs
Styles
Stepper - Bullet
DES DEV

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.

Properties
Steps
Current
Colors
Active dot #005CE5
Inactive dot #D2E5FF
Layout
Dot size 8 × 8
Gap between dots 8
Corner radius full (circle)
Stepper Bullet — Colors

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 PropertySwiftUICompose
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
RequirementiOSAndroid
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 N highlighted 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.

#Sibling componentNodeFrame (w × h)Highlighted variants
1Stepper - Bullet - 3 Steps27:4823580 × 1283 (highlighted = 1st, 2nd, 3rd)
2Stepper - Bullet - 4 Steps27:4825496 × 1644 (highlighted = 1st, 2nd, 3rd, 4th)
3Stepper - Bullet - 5 Steps27:48287112 × 2005 (highlighted = 1st, 2nd, 3rd, 4th, 5th)
1.0.0 — April 2026Major
Initial Assessment · canonical node 27:48287 (5 Steps) + 2 siblings (27:48254 4 Steps, 27:48235 3 Steps)
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
C7