Skip to content

feat(core): let Stepper collapse itself in narrow containers - #5659

Open
ernestt wants to merge 2 commits into
mainfrom
core-stepper-narrow-collapse
Open

feat(core): let Stepper collapse itself in narrow containers#5659
ernestt wants to merge 2 commits into
mainfrom
core-stepper-narrow-collapse

Conversation

@ernestt

@ernestt ernestt commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

A horizontal Stepper had no answer for narrow widths, so every consumer hand-rolled the same fallback: a media query, a ProgressBar, and a line of "Step 2 of 4" text standing in for the component. Four wizard templates carried a version of it, each with its own breakpoint.

The Stepper now measures its own frame and collapses once a step has under ~112px to work with. Labels give way to a bare track, and the current step is named in a row beneath it, flanked by prev/next controls when onStepClick makes them meaningful.

Measuring the frame rather than the viewport means the breakpoint follows the step count, so a stepper collapses on the width it actually has rather than the width of the window around it. Four steps hold out to ~448px; seven need ~784px.

The two indicator positions collapse differently, deliberately. separated gives up its click targets along with its labels, since all that would be left to aim at is a 4px bar — the paired controls become the way through. on-track keeps its indicators as a row of pressable nodes on the rail, which is the better affordance and costs nothing to leave in place.

No child introspection. Steps register with the parent to report the count, and the active step portals its own summary into a slot the Stepper provides. Nothing reads child.props or walks Children.

The sequence stays whole for screen readers at every width — collapsed steps keep visually hidden names, so the ordered list is never silently shortened.

Two fixes the collapse work surfaced, folded in because they only matter here:

  • Step labels hold to one line and ellipsize instead of wrapping and breaking mid-word, so a row of steps keeps one height and the track under it stays straight. The full label still reaches the accessible name.
  • The gap between connector segments is now --spacing-1, matching the connector's own thickness, so the track reads as one dashed line at any theme scale.

Scope

Core only. The wizard templates that motivated this still carry their hand-rolled fallbacks and are being cleaned up one PR per template. An unrelated LayoutContent padding fix in my working tree is deliberately left out.

Test plan

  • 58 Stepper tests pass, including a new block covering the collapse: labels dropping, the summary appearing with the current step's name and description, vertical steppers staying unaffected, the accessible sequence staying whole, controls bounding at both ends, collapsed steps leaving the tab order, and on-track nodes staying pressable.
  • pnpm -F @astryxdesign/core typecheck and pnpm -F @astryxdesign/storybook typecheck clean.
  • Lint clean on the changed files.
  • Verified in a browser at 1280 / 900 / 480 / 380px against a real wizard, driving the flow with both the form's own Continue and the stepper's controls, and confirming an errored step surfaces in the collapsed summary.
  • Truncation measured live: every label reports one line, with clipping starting exactly when its slice gets too narrow.
  • Gap measured live at 4px.
  • Reviewer: the new Narrow — Collapsed Track story exercises all four cases side by side.

Known gap, not addressed here

status never recolors the connector — progress is the bar's channel, status is the indicator's. That holds when every step shows an indicator, but collapsing removes indicators for every step except the current one, so an error on a step you have navigated away from is invisible while collapsed. In the labelled layout that step keeps its glyph. Worth a follow-up; I did not want to quietly break the channel separation to paper over it.

Made with Cursor

Screenshots

Separated, wide — unchanged. Four labelled steps, icons as indicators.

separated wide

Separated, 380px — collapsed. Labels and click targets gone, four bars left, the current step named underneath between two flat controls. Previous is disabled on step 1.

separated narrow

Separated, 380px — error on the current step. The summary carries the error glyph; Previous is now live.

separated error

On-track, wide.

on-track wide

On-track, 380px — collapsed differently, on purpose. The numbered nodes stay on the rail and stay pressable; all four remain hit-targets. Only the labels go.

on-track narrow

Label truncation. Captured with deliberately long labels. Each holds one line and ellipsizes, so the row keeps one height and the track stays straight; short labels are untouched.

truncation

A horizontal Stepper had no answer for narrow widths, so every consumer
hand-rolled the same fallback: a media query, a ProgressBar, and a line of
"Step 2 of 4" text standing in for the component. Four wizard templates
carried a version of it, each with its own breakpoint.

The Stepper now measures its own frame and collapses once a step has under
~112px to work with. Labels give way to a bare track and the current step is
named in a row beneath it, flanked by prev/next controls when `onStepClick`
makes them meaningful. Measuring the frame rather than the viewport means the
breakpoint follows the step count, so the collapse happens on the width the
stepper actually has rather than the width of the window around it.

The two indicator positions collapse differently, deliberately. `separated`
gives up its click targets with its labels, since all that would be left to
aim at is a 4px bar; the paired controls become the way through. `on-track`
keeps its indicators as a row of pressable nodes on the rail, which is the
better affordance and costs nothing to leave in place.

Steps register with the parent to report the count, and the active step
portals its own summary into a slot the Stepper provides, so none of this
needs child introspection. For screen readers the ordered list stays whole at
every width: collapsed steps keep visually hidden names, so the sequence is
never silently shortened.

Also fixes two things the collapse work surfaced. Step labels now hold to one
line and ellipsize instead of wrapping and breaking mid-word, which keeps a
row of steps at one height and the track under them straight; the full label
still reaches the accessible name. And the gap between connector segments is
now --spacing-1, matching the connector's own thickness, so the track reads
as one dashed line at any theme scale.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 28, 2026 6:20pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 28, 2026
@github-actions github-actions Bot added the needs:code-review High-risk change (new package/component/API) — needs human code review before merge label Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

Modified Components

Stepper (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1437 -
Complexity N/A Very High (155) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.8KB 1.2KB

Accessibility Audit

Status: 1 accessibility violation(s) found — 1 serious.

Stepper - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 1/29 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Visual Regression

2 of 406 shot(s) changed. View the report

To accept these exact frames: /accept-visual 33198611074/1 <reason>

A change here is a question, not a failure: check whether the after is the
picture you intended. If it is, say so in the PR — the release gate's baseline
is updated deliberately, and this check never rewrites it.

component story theme mode pixels
Stepper Default neutral light 5,451
Stepper Default neutral dark 5,325
Stepper — Default — neutral light
BeforeAfterDiff
Before visual regression frame After visual regression frame Pixel difference frame
Stepper — Default — neutral dark
BeforeAfterDiff
Before visual regression frame After visual regression frame Pixel difference frame

Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

The collapse work wraps the list in a frame and adds a summary row, both
carrying themeProps, but theming.targets never learned about them. Theme
authors and codegen read that array to discover which selectors exist, so
an undocumented class is an unthemeable element — which themingTargets
caught. Adds astryx-stepper-frame and astryx-stepper-summary to both the
English and Chinese target lists.

Co-authored-by: Cursor <cursoragent@cursor.com>
@xinzi-bot

Copy link
Copy Markdown

What this change fixes at narrow widths

I rendered the same 320px fixture on current main (88fde9103f) and this PR (21ba26155e). Each image contains the same three horizontal Stepper scenarios with the same active step and labels.

Before — main After — this PR
Before: horizontal steppers keep every long label and description in a 320px container After: horizontal steppers collapse to a compact track and active-step summary in a 320px container

Scenario-by-scenario

  1. Separated · navigable

    • Before: all four labels and their click targets compete for roughly 80px each, so labels wrap and the row grows.
    • After: the Stepper becomes a bare segmented track. Only the active step is named below it, with Previous/Next controls as usable narrow-width targets.
  2. On-track · navigable

    • Before: the numbered rail and all four labels remain visible in the same narrow space.
    • After: the labels collapse, but the numbered nodes stay visible and directly pressable; the active step is also named below the rail.
  3. Separated · form-driven

    • Before: every step's label and description remains in the narrow row, while the form's Back/Continue controls sit below it.
    • After: only the active step's label and description are shown. The Stepper does not add its own navigation controls because there is no onStepClick; the form's existing Back/Continue buttons remain the sole controls.

This is the core value of the PR: consumers no longer need to replace a crowded horizontal Stepper with their own media-query + progress-bar fallback. The component adapts based on its actual container width and step count.

@ernestt

ernestt commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

/accept-visual 33198611074/1 4px segments the individual steps more clearly instead of one continuous line

@github-actions

Copy link
Copy Markdown
Contributor

Visual acceptance could not be recorded. See this workflow run for the rejected invariant or infrastructure error.

@xinzi-bot

Copy link
Copy Markdown

Changes suggested (non-maintainer review)

The responsive collapse is the right idea, and the horizontal single-line improvement works. In a measured fixture, the uneven [42, 62, 42, 42] step heights become a uniform [42, 42, 42, 42].

@xinzi-bot is not a maintainer, so this is advisory and does not change the PR's review state. I found four newly introduced regressions that are worth addressing before landing:

  1. Vertical labels now overflow. The new whiteSpace: 'nowrap' applies to vertical Steppers too, but verticalBody lacks minWidth: 0, so the ellipsis cannot engage. At 150px, “Shipping address” previously wrapped and fit; it now extends beyond the container. otBodyV already has the needed floor release. (code)

  2. Compact controls can navigate onto a disabled step. With four steps and step 2 disabled, Next at 320px calls onStepClick(2) and moves aria-current there. At full width, that same step correctly has no button. (code)

  3. Compact mode unmounts Step.children. The conditional around contentNode drops the public content slot, not just the label. A form input disappears from the DOM at the compact threshold, and resizing destroys its state and focus. (code)

  4. The measured box is not the styled box. style, xstyle, className, and ref remain on the <ol>, while the new frame is measured. A Stepper constrained to 300px inside a 1000px parent therefore measures 1000px and does not collapse. It would be better to settle the outer-element contract now, following the existing TabList pattern. (code)

Non-blocking follow-ups: long summary labels can overlap the chevron; endContent/“Optional” are absent from the compact accessibility tree; an out-of-range activeStep leaves an empty summary; compact touch targets are 32px; focus is lost across the threshold; and the responsive behavior needs documentation.

The feature does not need redesigning, but these four cases warrant another revision before acceptance.

@xinzi-bot xinzi-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review found three compact-mode contract regressions: resizing unmounts Step content and can lose state or focus; compact controls can select disabled steps; and responsive measurement reads a wrapper instead of the public styled root. Preserving content/state, disabled semantics, and root sizing would keep existing contracts.

[Automated review]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:code-review High-risk change (new package/component/API) — needs human code review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants