Skip to content

feat(progresscircle): ensure s2 visual fidelity#6151

Merged
caseyisonit merged 4 commits intoswc-1668/poc-componentsfrom
marissahuysentruyt/swc-1670-progress-circle
Apr 17, 2026
Merged

feat(progresscircle): ensure s2 visual fidelity#6151
caseyisonit merged 4 commits intoswc-1668/poc-componentsfrom
marissahuysentruyt/swc-1670-progress-circle

Conversation

@marissahuysentruyt
Copy link
Copy Markdown
Collaborator

@marissahuysentruyt marissahuysentruyt commented Apr 7, 2026

Description

Cleans up progress circle styles in 2nd-gen. Also provides new accessibility improvements according to the a11y migration analysis.

  • Exports ProgressCircleSize type from @spectrum-web-components/core/components/progress-circle
  • Replaces hardcoded size templates in Sizes story with a PROGRESS_CIRCLE_VALID_SIZES.map() similar to badge and divider
  • Defines m as the default in the storybook control table
  • Adds new prefers-reduced-motion CSS media query
  • Creates dashOffset variable to ensure contrast is passed even when the progress is as 0% (notes in the code)

Motivation and context

The ProgressCircleSize type was already inferred in the types file but not exported, making it unavailable for stories and external consumers. Aligns with the pattern established in the badge component. The accessibility improvements set the component up for immediate success instead of bandaging something together later in life.

Related issue(s)

  • fixes swc-1670

Screenshots (if appropriate)


Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published.
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Includes thoughtfully written changeset if changes suggested include patch, minor, or major features
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers
  • All VRTs are approved before the author can update Golden Hash

Manual review test cases

Use these tables as a quick reference when validating the token usage in the browser!

Track color — --swc-progress-circle-track-border-color

Condition Token
Default track-color
static-color="white" static-white-track-color
static-color="black" static-black-track-color

Fill (indicator) color — --swc-progress-circle-fill-border-color

Condition Token
Default accent-content-color-default
static-color="white" static-white-track-indicator-color
static-color="black" static-black-track-indicator-color

Note: static-color="black" is new in S2. S1 only supported static-color="white".

Size-specific tokens

Property Token hook s m (default) l
Size --swc-progress-circle-size progress-circle-size-small progress-circle-size-medium progress-circle-size-large
Stroke width --swc-progress-circle-thickness progress-circle-thickness-small progress-circle-thickness-medium progress-circle-thickness-large
  • Progress circle stories render correctly at all sizes

    1. Go to the progress circle Storybook (Progress circle > Sizes)
    2. Verify all three sizes (s, m, l) render with the appropriate custom properties listed above.
  • Progress circle stories render correct color tokens

    1. Go to the progress circle Storybook docs page
    2. Inspect a default (non-static color) progress circle
    3. Verify the track and fill colors match the default tokens listed in the tables above.
    4. Inspect a static white progress circle in your browser.
    5. Verify the track and fill colors match the static white tokens listed in the tables above.
    6. Inspect a static black progress circle in your browser.
    7. Verify the track and fill colors match the static black tokens listed in the tables above.
  • Indeterminate story renders with animation

    1. Go to Progress circle > Indeterminate
    2. Verify the animated spinner is visible and the Processing request label is set
    3. Expect no aria-valuenow attribute on the host element
  • WHCM styles in AssistivLabs

    1. Go to the progress circle playground story in AssistivLabs or use the forced-colors emulator in Chrome
    2. Verify the track-border color and fill-border-color are visible in both dark and light high contrast modes. All variants (default, static white, static black) should appear identical in WHCM.
  • Reduced motion

    1. Go to the progress circle playground story and change the indeterminate control to true
    2. Verify you see the progress circle animation.
    3. In your computer settings, (System Settings > Accessibility > Motion if you're on a Mac), and turn on the Reduced Motion control.
    4. Verify the indeterminate progress circle animation follows your user setting and stops the animation. (example below!)
Screen.Recording.2026-04-07.at.4.21.39.PM.mov

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

Accessibility testing checklist

Required: Complete each applicable item and document your testing steps (replace the placeholders with your component-specific instructions).

  • Keyboard
  1. Go to Progress circle > Playground in Storybook
  2. Confirm the component is not focusable (no interactive parts)
  • Screen reader (required — document steps below) — What to test for: Role and name are announced correctly; state changes (e.g. expanded, selected) are announced; labels and relationships are clear; no unnecessary or duplicate announcements.
  1. Go to Progress circle > Playground in Storybook with a screen reader active (VoiceOver/NVDA)
  2. It might be helpful to open the canvas in a fresh tab (outside of the iframe)
  3. Navigate to the progress circle element
  4. Expect the role progressbar to be announced, the aria-label to match the label arg (e.g. "Uploading
    document"), and aria-valuenow / aria-valuetext to reflect the current progress value
Screenshot 2026-04-07 at 2 02 58 PM
  1. Switch to the Indeterminate story and navigate to the element
  2. Expect the role progressbar to be announced with the label, but no value (no aria-valuenow)
Screenshot 2026-04-07 at 2 08 25 PM

NOTE: reduced motion testing steps are in the default "Manual testing" section

@marissahuysentruyt marissahuysentruyt self-assigned this Apr 7, 2026
@marissahuysentruyt marissahuysentruyt added Component:Progress circle Spectrum CSS 2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. labels Apr 7, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 7, 2026

⚠️ No Changeset found

Latest commit: 5b8512c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

📚 Branch Preview Links

🔍 First Generation Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6151

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

@marissahuysentruyt marissahuysentruyt added the Status:WIP PR is a work in progress or draft label Apr 7, 2026
.swc-ProgressCircle {
--swc-progress-circle-fill-border-color: Highlight;
--swc-progress-circle-track-color: Canvas;
--swc-progress-circle-track-border-color: Canvas;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@5t3ph do you think we could delete this Canvas definition for the track color? I think Canvas effectively "removes" the track, making it not visible. The additional dark and light queries also seem to be overriding Canvas anyways in AssistivLabs. Are you experiencing the same thing?

Image Image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you possibly resolve this already? I see the transparent values coming through in both "light" and "dark" themes. Since we are overwriting that var inside the nested light vs. dark queries, the initial assignment being pointed out here isn't being used.
image
image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I didn't resolve anything yet, but I wanted to make sure I could remove that initial assignment to Canvas! I was seeing the light/dark theme transparent values like you screenshotted as well! Canvas was being overwritten by light/dark no matter what theme I was in.

I'm going to remove that initial track-border-color and just rely on the light/dark definitions. 👍

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following 👀 and will make sure that #6146 doesn't conflict, I removed that custom prop because it wasn't being used.

Comment on lines +107 to +109
@media (prefers-reduced-motion: reduce) {
.swc-ProgressCircle--indeterminate .swc-ProgressCircle-fill {
animation: none;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added the reduced-motion query since it was in the a11y migration guide. Do we have a philosophy on reduced-motion? Right now, if a user has reduced motion turned on, it just doesn't do the indeterminate animation. Is that what we are envisioning?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@nikkimk just wanted to tag you here since I made some of the accessibility updates. I'd love to get your opinions on the reduced-motion and then the dashOffset concept so that even an empty/0% progress circle still shows a bit of the fill to meet contrast.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The visual effect of none is a filled-in indicator, which seems too extreme and loses meaning.

Image

I think we would have a couple options:

  1. also add stroke-dashoffset: 75px (or other value) to show a partial indicator to keep meaning of a load in-progress
  2. instead of stopping animation, instead slow it waaaay down and have it be linear rather than the "jump/catch-up" effect it has to remove the "blinking/flashing" effect
reduced-motion-progress-indicator.mov

Comment on lines +79 to +88
// At progress=0, a dashoffset of 100 makes the fill fully invisible, which fails WCAG 1.4.11
// non-text contrast (the track alone may not meet 3:1 against the background).
// Clamp to 98 to show a minimum 2-unit fill so the graphical element remains perceivable.
// aria-valuenow stays at 0 — this is a visual-only adjustment.
const dashOffset = this.indeterminate
? 100 - this.progress
: this.progress === 0
? 98
: 100 - this.progress;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

In the a11y migration, I saw this:

When **`progress`** is **0**, show a **small** filled segment of the circle (or another treatment that keeps **graphical** ring details at **at least 3:1** with adjacent colors). A **fully empty** ring at **0%** often fails **non-text contrast** because the **track** alone is too weak against the background.

So, that's what this dashOffset is attempting to do. Do we have opinions on like, how much of the fill we'd like to continue to show when the value is 0? 98 is a completely magic number. ✨ ✨ ✨

The 0% fill is the first one in this set.

Image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

turned this in to a private method with jsdocs

[`swc-ProgressCircle--indeterminate`]: this.indeterminate,
[`swc-ProgressCircle--static${capitalize(this.staticColor)}`]:
typeof this.staticColor !== 'undefined',
[`swc-ProgressCircle--size${this.size?.toUpperCase()}`]:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We don't need the size classes on the internal wrapper div.

@marissahuysentruyt marissahuysentruyt marked this pull request as ready for review April 7, 2026 20:52
@marissahuysentruyt marissahuysentruyt requested a review from a team as a code owner April 7, 2026 20:52
@marissahuysentruyt marissahuysentruyt added Status:Ready for review PR ready for review or re-review. and removed Status:WIP PR is a work in progress or draft labels Apr 7, 2026
@marissahuysentruyt marissahuysentruyt force-pushed the swc-1668/poc-components branch from 419e8bf to 5038447 Compare April 8, 2026 13:08
rise-erpelding added a commit that referenced this pull request Apr 8, 2026
This reverts commit db5a16f.

This commit and the commit that it reverts will be dropped if this
branch needs a rebase if #6151 goes in first.
@marissahuysentruyt marissahuysentruyt force-pushed the marissahuysentruyt/swc-1670-progress-circle branch from f635672 to 181944f Compare April 8, 2026 22:30
Comment on lines +288 to +292
* #### Non-interactive element
*
* - Progress circles have no interactive behavior and are not focusable
* - Screen readers will announce the progress circle content as static text
* - No keyboard interaction is required or expected
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@nikkimk is this the right language for progress circles? I'm most concerned with the screen reader bullet point (like is it "static text" or something else?)

@rise-erpelding rise-erpelding force-pushed the swc-1668/poc-components branch from a099390 to 06a2855 Compare April 9, 2026 13:42
rise-erpelding added a commit that referenced this pull request Apr 9, 2026
This reverts commit db5a16f.

This commit and the commit that it reverts will be dropped if this
branch needs a rebase if #6151 goes in first.
nikkimk
nikkimk previously requested changes Apr 9, 2026
Copy link
Copy Markdown
Contributor

@nikkimk nikkimk left a comment

Choose a reason for hiding this comment

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

I have some requested changes based on these proposed design guidelines: https://www.figma.com/design/42VzvpW262EAUbYsadO4e8/Loading-animation-discovery

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The SVG is showing up in the a11y tree as an image with no alt text. Try this:

Suggested change
<svg aria-hidden="true" fill="none" width="100%" height="100%" class="swc-outerCircle">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be deprecated. A progress circle should be indeterminate until a value is given, so we can set indeterminate animation based on whether value is undefined.

New guidelines for progress bars and circles is that they are indeterminate by default unless there is a value given:

https://www.figma.com/design/42VzvpW262EAUbYsadO4e8/Loading-animation-discovery?node-id=478-948465&t=7bf2ViRr4MWMGhre-0

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

There was a similar suggestion in the code styles PR, and also similarly, it feels out of scope for this ticket. Could we add another todo in the comments, referencing ticket 1891.

Does that work for now while the poc feature branch and epic are both still wrapping up?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i went ahead and refactored it in this PR

Comment thread 2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts Outdated
Comment thread 2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts Outdated
Comment thread 2nd-gen/packages/swc/components/progress-circle/ProgressCircle.ts Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
* - Removes `aria-valuenow` when no value is given.

@marissahuysentruyt marissahuysentruyt force-pushed the marissahuysentruyt/swc-1670-progress-circle branch from 181944f to b524d1f Compare April 10, 2026 16:19
@marissahuysentruyt marissahuysentruyt removed their assignment Apr 10, 2026
@cdransf cdransf force-pushed the swc-1668/poc-components branch from e0d3061 to b446021 Compare April 14, 2026 17:03
- Exports `ProgressCircleSize` type from core
- Adds `prefers-reduced-motion` CSS media query
- Adds `dashOffset` variable to ensure contrast at 0% progress
- Replaces hardcoded size templates with `PROGRESS_CIRCLE_VALID_SIZES.map()`
- Removes sizing antipattern in render
- Removes canvas WHCM track color
- Implements a11y improvements per migration analysis

Also includes minor cross-component cleanup:
- Renames `STATUSLIGHT_*` constants to `STATUS_LIGHT_*` convention
- Adds `{ type: String }` to `@property()` decorators in Asset and Icon base classes
- Fixes `customElements` path in swc package.json
- Tags icon internal stories as migrated
- Updates Storybook source type from auto to dynamic
- Minor avatar stories and status-light stories updates

fixes swc-1670

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cdransf cdransf force-pushed the marissahuysentruyt/swc-1670-progress-circle branch from 0c46b62 to 0a87ab4 Compare April 15, 2026 22:08
@caseyisonit caseyisonit dismissed nikkimk’s stale review April 17, 2026 17:12

@nikkimk im going to dismiss this changes requested to unblock this merging to the feature branch. you will be a required reviewer in the final PR review but i have applied all of your feedback. Hope you feel better!!

Copy link
Copy Markdown
Contributor

@5t3ph 5t3ph left a comment

Choose a reason for hiding this comment

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

None of these are blocking for MVP, so adding approval.

* 3. **Label** - Accessible text describing the operation (not visually rendered).
*
* ### Content
* > **A11y Note:** Light DOM children are not projected into the shadow tree, so content between the opening and closing tags does not supply an accessible name. Use the `label` attribute or property, or `aria-label` / `aria-labelledby` on the host.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[todo] we perhaps should have a take-away here to figure out patterns for "callouts" especially since the semantics of this being a blockquote are imperfect (although handy for markdown)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i agree! i will flag this in a comment. i think this also ties nicely into the CEM additions i would like to introduce and we can create a custom doc block for rendering these in a consistent manner

* @property {boolean} indeterminate - Indeterminate state for loading.
* @property {number | null} progress - Progress between 0 and 100, reflected as the `progress` attribute when set. When `null` (indeterminate), the attribute is omitted.
* @property {string} size - Size of the component.
* @property {string} label - Label for the component.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(this can be a follow-up/possible group discussion): for button migration, I am suggesting dropping label in favor of the appropriate aria attributes only to simplify the API / make intent more explicit, and also not muddy "label" in case it ends up used as a slot name ever.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OMG i had the same thought!! i actaully would like to do that for progress circle because that actually aligns more with the implementation and usage of RSP and good semantic a11y HTML

Comment on lines +107 to +109
@media (prefers-reduced-motion: reduce) {
.swc-ProgressCircle--indeterminate .swc-ProgressCircle-fill {
animation: none;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The visual effect of none is a filled-in indicator, which seems too extreme and loses meaning.

Image

I think we would have a couple options:

  1. also add stroke-dashoffset: 75px (or other value) to show a partial indicator to keep meaning of a load in-progress
  2. instead of stopping animation, instead slow it waaaay down and have it be linear rather than the "jump/catch-up" effect it has to remove the "blinking/flashing" effect
reduced-motion-progress-indicator.mov

Copy link
Copy Markdown
Member

@cdransf cdransf left a comment

Choose a reason for hiding this comment

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

A few minor things, but looks great! ✨

style: 'percent',
unitDisplay: 'narrow',
}).format(this.progress / 100);
this.setAttribute('role', 'progressbar');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason we removed the guard here? Would this remove whatever the user/consumer sets? ✨

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

good catch this was not intended to be removed, i will add this back in!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

oh after looking at this, the format progess is handled in the updated() to make sure it stays in sync with the reflective attribute which with trigger an updated lifecycle. the formatProgress method was moved up to the methods section since it was sandwiched between two lifecycles. its still present

size: 'l',
label: 'Upload complete',
})}
render: () => html`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we need to pass args here to wire up the controls ✨

Suggested change
render: () => html`
render: (args) => html`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ill wire it up with args, will need to spread them in the components so ill do it in a commit

*/
export const Indeterminate: Story = {
export const ProgressValues: Story = {
render: () => html`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We lose the storybook controls here too. ✨

Suggested change
render: () => html`
render: (args) => html`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same as a above, this will come via a new commit

} else {

if (changes.has('progress')) {
if (this.progress !== null && this.progress >= 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can simplify this ✨

Suggested change
if (this.progress !== null && this.progress >= 0) {
if (this.progress !== null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

actually it is needed because setting this.progree === 0 also can compile to null so this makes sure its reading zero as a value and not as a falsey statement

@caseyisonit caseyisonit merged commit b147afa into swc-1668/poc-components Apr 17, 2026
22 of 26 checks passed
@caseyisonit caseyisonit deleted the marissahuysentruyt/swc-1670-progress-circle branch April 17, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. Component:Progress circle Spectrum CSS Status:Ready for review PR ready for review or re-review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants