Stop ToggleSwitch jumping when the loading spinner is shown - #4152
Conversation
… jump when loading Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 14f13505-d338-494c-a7df-96616e27c88f
🦋 Changeset detectedLatest commit: f90fc6a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
… no hidden IDL property Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 14f13505-d338-494c-a7df-96616e27c88f
There was a problem hiding this comment.
Pull request overview
Fixes a layout-shift bug in Primer::Alpha::ToggleSwitch (small variant) where revealing the loading/error status icon changes the inline-flex baseline and causes the switch to “jump” vertically.
Changes:
- Reserve vertical space for
.ToggleSwitch-statusIcon(height + centering) to prevent baseline changes when icons appear. - Add system tests that assert the switch track’s
getBoundingClientRect().topis stable when the spinner or error icon is unhidden. - Add a patch changeset documenting the fix.
Show a summary per file
| File | Description |
|---|---|
| test/system/alpha/toggle_switch_test.rb | Adds system regression tests ensuring the small ToggleSwitch doesn’t shift when status icons are shown. |
| app/components/primer/alpha/toggle_switch.pcss | Reserves height and centers contents in the status icon slot to eliminate vertical jumping. |
| .changeset/hungry-poems-shout.md | Publishes a patch-level changeset describing the CSS fix. |
Review details
Suppressed comments (2)
test/system/alpha/toggle_switch_test.rb:138
- Like the spinner test, this
execute_scriptwill fail with an unhelpful JS error if the element can’t be found. Adding an explicit existence assertion (visible: :all) and a clearer error improves debuggability.
page.execute_script(
"document.querySelector(\"[data-target='toggle-switch.errorIcon']\").removeAttribute('hidden')"
)
test/system/alpha/toggle_switch_test.rb:148
track_topwill currently raise a JS TypeError if.ToggleSwitch-trackis not found, which makes failures harder to interpret. Throwing a targeted error produces clearer test output if the preview structure changes.
def track_top
evaluate_script("document.querySelector('.ToggleSwitch-track').getBoundingClientRect().top")
end
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
…nt fails clearly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 14f13505-d338-494c-a7df-96616e27c88f
llastflowers
left a comment
There was a problem hiding this comment.
Verified that the jump can be observed here (on first load only) https://primer.style/view-components/lookbook/inspect/primer/alpha/toggle_switch/small/
Nothing stands out as a potential regression risk 👍
liuliu-dev
left a comment
There was a problem hiding this comment.
nitpick: consider adding line-height: var(--base-size-16) here as well.
Screen.Recording.2026-07-31.at.10.09.57.AM.mov
…sits on centre Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 14f13505-d338-494c-a7df-96616e27c88f
|
Good catch! Went with
|
|
@llastflowers just in case you're curious, in the wild it happens on every click – at least in this pattern inside a control group Screen.Recording.2026-07-31.at.21.56.03.mov |
Merges primer/view_components up to 18aa9c4, the batch released upstream as v0.53.2. Component change: reserves height as well as width in .ToggleSwitch-statusIcon so the switch no longer jumps when the loading spinner appears (primer#4152). Keeps the fork's workflow files unchanged: the upstream conflicts are GitHub Action version bumps (the fork pins by SHA through its own Dependabot) and the migration of gem publishing to RubyGems Trusted Publishing, which does not apply to the fork's release flow. Replays the rails-html-sanitizer 1.7.1 and loofah 2.25.2 bumps that merge=ours dropped from both Gemfile.locks. Remaining upstream lockfile bumps are left to the fork's weekly Dependabot. Regenerates static/classnames.* from a clean tree, which also picks up class names the fork's own sources already carried.
Merges primer/view_components up to 18aa9c4, the parent of upstream Release Tracking primer#4154. Component change: reserves height as well as width in .ToggleSwitch-statusIcon so the switch no longer jumps when the loading spinner appears (primer#4152). Keeps the fork's workflow files unchanged: the upstream conflicts are GitHub Action version bumps (the fork pins by SHA through its own Dependabot) and the migration of gem publishing to RubyGems Trusted Publishing, which does not apply to the fork's release flow. Replays the rails-html-sanitizer 1.7.1 and loofah 2.25.2 bumps that merge=ours dropped from both Gemfile.locks. Remaining upstream lockfile bumps are left to the fork's weekly Dependabot. Regenerates static/classnames.* from a clean tree, which also picks up class names the fork's own sources already carried.
What are you trying to accomplish?
Fixes the long-standing bug where a
ToggleSwitchjumps up 4px the moment its loading spinner appears, then drops back down when the request finishes. Onlysize: :smallis affected, which is what #2623 reported in 2024. It resurfaced this week on GitHub's Code scanning settings page, but everyToggleSwitchwith asrcis affected.Root cause.
.ToggleSwitch-statusIconreserves width but not height:With both icons hidden the box has no in-flow children, so it's 0px tall. Unhiding the spinner makes it 21px, not 16px, because
Primer::Beta::Spinnerhangshiddenon a wrapper<span>that also carriessr-only"Loading" text, and that inline formatting context adds the font strut descender..ToggleSwitchisinline-flex, so it sits on its line by its baseline, which comes from its first flex item, this box. 0px to 21px moves the baseline and the browser repositions the whole switch. At the default size the 32px track stays the tallest item so nothing moves. Atsmallthe track is 24px and the 21px slot wins, hence -4px.The fix reserves the height the same way the width is already reserved, and centers the contents.
Screenshots
No visual change at rest. The change is that the switch stops moving while loading.
Measured on this PR's preview deployment,
.ToggleSwitch-tracktopbefore and after unhiding each icon. Right column is the same page with the new rule overridden back to what ships today:Spinner and error icon give identical numbers in each.
Integration
CSS only, no API or markup change. Consumers pick it up on the next release.
List the issues that this change affects.
Closes #2623
Risk Assessment
One rule, one component. The switch's own box height is unchanged (24px small, 32px default) so nothing around it reflows.
What approach did you choose and why?
The slot already reserves width so the switch can't move horizontally. This makes it reserve height for the same reason.
Two alternatives I measured and discarded:
line-height: 0on the slot. Collapses the spinner wrapper 21px to 16px, but the box still goes 0px to 16px, so the baseline still moves and you still get the full -4px.Primer::Beta::Spinnerby giving its wrapperdisplay: inline-flex. Worth doing on its own merits, but it doesn't fix this bug for the same reason. Happy to open that separately.heightalone gets to 0px.flex: noneand the centering are belt and braces so the 21px wrapper can't stretch the box when the row is tight.Anything you want to highlight for special attention from reviewers?
The tests unhide the icons directly rather than clicking and racing the in-flight request, so they assert the CSS invariant instead of flaking on timing. Both fail without the CSS change, per the right column above.
They use
removeAttribute('hidden')rather than.hidden = falsebecausehiddenis anHTMLElementIDL property and the error icon is an<svg>, so assigning to it silently does nothing. CI caught that on my first attempt.Accessibility
CSS only, no change to markup, roles, or the
sr-onlyloading text.Merge checklist
Docs and previews aren't affected, the existing
smallpreview already reproduces the bug.