From 393806856b96f77a179f40df52aba0d0f2671e3c Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Mon, 24 Aug 2026 13:36:25 -0400 Subject: [PATCH 01/13] Add spinner guide --- guides/ui-components/progress-ring/demo.html | 184 ++++++++++++++++++ .../progress-ring/expectations.md | 14 ++ guides/ui-components/progress-ring/guide.md | 164 +++++++++++++++- guides/ui-components/spinner/demo.html | 154 +++++++++++++++ guides/ui-components/spinner/expectations.md | 11 ++ guides/ui-components/spinner/guide.md | 95 ++++++++- 6 files changed, 620 insertions(+), 2 deletions(-) create mode 100644 guides/ui-components/progress-ring/demo.html create mode 100644 guides/ui-components/progress-ring/expectations.md create mode 100644 guides/ui-components/spinner/demo.html create mode 100644 guides/ui-components/spinner/expectations.md diff --git a/guides/ui-components/progress-ring/demo.html b/guides/ui-components/progress-ring/demo.html new file mode 100644 index 000000000..cd8adb68a --- /dev/null +++ b/guides/ui-components/progress-ring/demo.html @@ -0,0 +1,184 @@ + + + + + + Progress Ring Demo + + + +
+
+
+ +
+
0%
+
+ +
+
+ + +
+
+
+ + + + diff --git a/guides/ui-components/progress-ring/expectations.md b/guides/ui-components/progress-ring/expectations.md new file mode 100644 index 000000000..adfdfdd47 --- /dev/null +++ b/guides/ui-components/progress-ring/expectations.md @@ -0,0 +1,14 @@ +# Expectations + +- The component contains a native HTML `` element. +- The component is visible on the page. +- The visual progress ring is implemented using a CSS `conic-gradient`. +- The center of the progress ring is transparent or "hollowed out" using `mask-image`. +- The `--value` CSS custom property is used to drive the visual progress of the ring. +- Methods that change the `value` of the `` element also updates the visual ring via the `--value` property. +- The component supports displaying text content (e.g., the percentage) in the center of the ring. +- The ring smoothly transitions between values when the `--value` property is updated (requiring `@property` support in the browser). +- The `` element includes an `aria-label` for accessibility. +- The `` element is visually hidden using a standard utility class (e.g., `.visually-hidden`). +- The component's fill color changes to a success color (e.g., green) when the progress value reaches 100%. +- The component has a distinct visual "track" (background) behind the progress fill. diff --git a/guides/ui-components/progress-ring/guide.md b/guides/ui-components/progress-ring/guide.md index 3f9c01fce..c12f75870 100644 --- a/guides/ui-components/progress-ring/guide.md +++ b/guides/ui-components/progress-ring/guide.md @@ -4,6 +4,168 @@ description: Build a progress ring component that visually represents the comple web-feature-ids: - progress - conic-gradients + - masks + - registered-custom-properties --- - +## Overview + +A progress ring (or circular progress bar) provides visual feedback on the status of a task. Unlike a linear progress bar, its circular shape is ideal for dashboards, card components, or anywhere space is constrained. + +This guide implements a progress ring by: + +- Using the native `` element as the semantic foundation to ensure the component is accessible to screen readers and keyboard users out-of-the-box. +- Styling the component with `conic-gradient()` and `mask-image`, allowing for a fully responsive and themeable ring without the complexity of SVG path manipulation. +- Leveraging CSS Custom Properties and `@property` to enable smooth, GPU-accelerated transitions of the progress fill. + +This approach is preferred over SVG-only solutions because it keeps the implementation entirely in CSS, making it easier to integrate with existing design systems and typography (especially for content in the center). + +## Implementation + +### 1. Markup + +Use a wrapper to hold both the visual ring and the optional center content. The `` element remains the semantic source of truth. Use a utility class to visually hide the native progress bar while keeping it accessible. + +```html +
+
+ +
+ +
+ 75% +
+
+``` + +Alternatively, you can choose to apply the `progressbar` role to the `.progress-ring` `
`, in which case you must set the minimum, maximum and current values using Aria. + +```html +
+``` + +### 2. Styles + +#### Container and Ring + +The wrapper provides the positioning context. The `progress-ring` element handles the visual gradient and mask. + +```css +.progress-ring-wrapper { + --size: 120px; + --thickness: 12px; + --track-color: #eee; + --fill-color: #3b82f6; + + position: relative; + display: grid; + place-items: center; + width: var(--size); + height: var(--size); +} + +.progress-ring { + width: 100%; + height: 100%; + border-radius: 50%; + + /* The progress fill is a conic gradient mapped to the --value */ + background: conic-gradient( + var(--fill-color) calc(var(--value) * 1%), + var(--track-color) 0 + ); + + /* Create the "ring" by masking out the center */ + mask-image: radial-gradient( + transparent calc(50% - var(--thickness)), + black calc(50% - var(--thickness) + 0.5px) + ); +} + +/* Standard utility to visually hide elements while keeping them accessible */ +.visually-hidden:where(:not(:focus-within, :active)) { + position: absolute !important; + clip-path: inset(50%) !important; + overflow: hidden !important; + width: 1px !important; + height: 1px !important; + margin: -1px !important; + padding: 0 !important; + border: 0 !important; + white-space: nowrap !important; +} + +.progress-ring-content { + /* Positioned in the center of the wrapper */ + position: absolute; +} +``` + +#### Enable smooth transitions with `@property` + +To animate the progress ring smoothly when the value changes, register `--value` as a numeric custom property. + +```css +@property --value { + syntax: ''; + inherits: true; + initial-value: 0; +} + +.progress-ring-wrapper { + transition: --value 0.3s ease-in-out; +} +``` + +### 3. Progress Updates + +Update the `--value` custom property on the wrapper whenever the `` value changes. + +```html + + + +``` + +### 4. Optional Success State + +You can use the CSS `:has()` pseudo-class to automatically update the ring's appearance (e.g., changing the color to green) when the task reaches completion. + +```css +/* Change the fill color to green when the progress reaches 100% */ +.progress-ring-wrapper:has(progress[value="100"]) { + --fill-color: #10b981; +} +``` + +## Fallback strategies + +The core components of this implementation — `` and `conic-gradient()` and `mask-image` with `radial-gradient()` — are Baseline Widely available. The registered `@property` for animation is the only modern addition. + +Do not add a fallback value inside the `` element. It is not used by assistive technology and ignored by all modern browsers. + +#### Animation fallback + +{{ FEATURE_FALLBACKS("registered-custom-properties") }} + +If `@property` is not supported, the ring will jump to the new value instantly instead of transitioning smoothly. This does not break the functionality. For browsers without `@property`, you can achieve transitions using a JavaScript `requestAnimationFrame` loop to interpolate the `--value`, though the native CSS transition is preferred for performance. + +See the [Spinner guide](../spinner/guide.md) for handling indeterminate loading states. diff --git a/guides/ui-components/spinner/demo.html b/guides/ui-components/spinner/demo.html new file mode 100644 index 000000000..24adf099d --- /dev/null +++ b/guides/ui-components/spinner/demo.html @@ -0,0 +1,154 @@ + + + + + + Spinner Demo + + + +
+
+
+
Default
+
+ +
+
+ +
+
Large
+
+ +
+
+ +
+
Custom
+
+ +
+
+
+
+ + diff --git a/guides/ui-components/spinner/expectations.md b/guides/ui-components/spinner/expectations.md new file mode 100644 index 000000000..ff70d1e06 --- /dev/null +++ b/guides/ui-components/spinner/expectations.md @@ -0,0 +1,11 @@ +# Expectations + +- The component contains a native HTML `` element. +- The `` element does not have a `value` attribute, correctly signaling an indeterminate state. +- The `` element includes an `aria-label` (e.g., "Loading") for accessibility. +- The `` element is visually hidden using a standard utility class (e.g., `.visually-hidden`). +- The component is visible on the page. +- The visual spinner is implemented using a CSS `conic-gradient` to create a fading trail. +- The center of the spinner is transparent or "hollowed out" using `mask-image`. +- The spinner continuously rotates using a CSS animation on the `transform` property. +- The spinner respects `prefers-reduced-motion` by slowing down the animation (e.g., increasing `animation-duration`). diff --git a/guides/ui-components/spinner/guide.md b/guides/ui-components/spinner/guide.md index 9a7ce2e56..0220c43c1 100644 --- a/guides/ui-components/spinner/guide.md +++ b/guides/ui-components/spinner/guide.md @@ -7,4 +7,97 @@ web-feature-ids: - progress --- - +## Overview + +A loading spinner (or activity indicator) informs users that a process is underway when the exact duration is unknown. Unlike a progress ring, a spinner is "indeterminate" and typically uses animation to signal activity. + +This guide implements a spinner by: + +- Using the native `` element as the semantic foundation. By omitting the `value` attribute, the browser treats it as an indeterminate progress bar, ensuring correct announcement by assistive technologies. +- Styling the component with `conic-gradient()` to create a visual "trail" and `mask-image` to hollow out the center into a ring. +- Animating the spinner efficiently using CSS transforms and respecting `prefers-reduced-motion` to ensure a comfortable experience for all users. + +See the [Progress Ring guide](../progress-ring/guide.md) for handling determinate tasks with a known duration. + + +## Implementation + +### 1. Markup + +We use a wrapper to hold the visual spinner. The `` element remains the semantic source of truth. Without a `value` attribute, it is implicitly indeterminate. + +```html +
+ + +
+``` + +Alternatively, you may choose to omit the `` element, and add the `status` ARIA role to the `.loading-spinner` `
` element. + +### 2. Styles + +#### Spinner Ring and Trail + +The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image` is used to create the ring shape. + +```css +.loading-spinner { + --size: 40px; + --thickness: 4px; + --spinner-color: #3b82f6; + + position: relative; + width: var(--size); + height: var(--size); + border-radius: 50%; + + /* Create a fading trail from the spinner color to transparent */ + background: conic-gradient( + from 0deg, + var(--spinner-color), + transparent 75% + ); + + /* Hollow out the center to create a ring */ + mask-image: radial-gradient( + transparent calc(50% - var(--thickness)), + black calc(50% - var(--thickness) + 0.5px) + ); + + /* Continuous rotation animation */ + animation: spinner-rotate 0.8s linear infinite; +} + +@keyframes spinner-rotate { + to { transform: rotate(360deg); } +} + +/* Standard utility to visually hide elements while keeping them accessible */ +.visually-hidden:where(:not(:focus-within, :active)) { + position: absolute !important; + clip-path: inset(50%) !important; + overflow: hidden !important; + width: 1px !important; + height: 1px !important; + margin: -1px !important; + padding: 0 !important; + border: 0 !important; + white-space: nowrap !important; +} +``` + +#### Respecting Motion Preferences + +Users with motion sensitivities may find fast-spinning elements disorienting. Always respect the `prefers-reduced-motion` media query. + +```css +@media (prefers-reduced-motion: reduce) { + .loading-spinner { + /* Slow down the animation significantly rather than stopping it entirely, + so the user still knows that the process is active. */ + animation-duration: 3s; + } +} +``` +Alternatively, replace the spinner with a static text label for users with `prefers-reduced-motion` enabled. From 4446b4bf5a8399cc3f32603ec88b28d91463f8a7 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Mon, 24 Aug 2026 22:47:53 -0500 Subject: [PATCH 02/13] Revisions --- guides/ui-components/progress-ring/demo.html | 5 ++++ .../progress-ring/expectations.md | 1 + guides/ui-components/progress-ring/guide.md | 26 +++++++++++------- guides/ui-components/spinner/demo.html | 27 +++++++++++-------- guides/ui-components/spinner/guide.md | 10 ++++--- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/guides/ui-components/progress-ring/demo.html b/guides/ui-components/progress-ring/demo.html index cd8adb68a..95d6ed27f 100644 --- a/guides/ui-components/progress-ring/demo.html +++ b/guides/ui-components/progress-ring/demo.html @@ -100,6 +100,11 @@ transition: --value 0.4s cubic-bezier(0.4, 0, 0.2, 1); } + @media (prefers-reduced-motion: reduce) { + .progress-ring-wrapper { + transition-duration: 0s; + } + } .progress-ring { width: 100%; diff --git a/guides/ui-components/progress-ring/expectations.md b/guides/ui-components/progress-ring/expectations.md index adfdfdd47..fb7bde82f 100644 --- a/guides/ui-components/progress-ring/expectations.md +++ b/guides/ui-components/progress-ring/expectations.md @@ -12,3 +12,4 @@ - The `` element is visually hidden using a standard utility class (e.g., `.visually-hidden`). - The component's fill color changes to a success color (e.g., green) when the progress value reaches 100%. - The component has a distinct visual "track" (background) behind the progress fill. +- The spinner respects `prefers-reduced-motion` by changing to a new value immediately, rather than having a smooth transition. \ No newline at end of file diff --git a/guides/ui-components/progress-ring/guide.md b/guides/ui-components/progress-ring/guide.md index c12f75870..f7262742a 100644 --- a/guides/ui-components/progress-ring/guide.md +++ b/guides/ui-components/progress-ring/guide.md @@ -18,7 +18,9 @@ This guide implements a progress ring by: - Styling the component with `conic-gradient()` and `mask-image`, allowing for a fully responsive and themeable ring without the complexity of SVG path manipulation. - Leveraging CSS Custom Properties and `@property` to enable smooth, GPU-accelerated transitions of the progress fill. -This approach is preferred over SVG-only solutions because it keeps the implementation entirely in CSS, making it easier to integrate with existing design systems and typography (especially for content in the center). +This approach is preferred over SVG-only solutions because it uses the semantic `` element rather than ARIA, and more easily integrates with existing layout, design systems and typography. + +See the {{ GUIDE_REF("spinner") }} for handling indeterminate loading states. ## Implementation @@ -38,12 +40,6 @@ Use a wrapper to hold both the visual ring and the optional center content. The
``` -Alternatively, you can choose to apply the `progressbar` role to the `.progress-ring` `
`, in which case you must set the minimum, maximum and current values using Aria. - -```html -
-``` - ### 2. Styles #### Container and Ring @@ -115,6 +111,7 @@ To animate the progress ring smoothly when the value changes, register `--value` .progress-ring-wrapper { transition: --value 0.3s ease-in-out; } + ``` ### 3. Progress Updates @@ -156,6 +153,19 @@ You can use the CSS `:has()` pseudo-class to automatically update the ring's app } ``` +### 5. Respecting Motion Preferences + +Users with motion sensitivities may find the transition between values disorienting. Respect the `prefers-reduced-motion` media query by transitioning immediately. + + +```css +@media (prefers-reduced-motion: reduce) { + .progress-ring-wrapper { + transition-duration: 0s; + } +} +``` + ## Fallback strategies The core components of this implementation — `` and `conic-gradient()` and `mask-image` with `radial-gradient()` — are Baseline Widely available. The registered `@property` for animation is the only modern addition. @@ -167,5 +177,3 @@ Do not add a fallback value inside the `` element. It is not used by a {{ FEATURE_FALLBACKS("registered-custom-properties") }} If `@property` is not supported, the ring will jump to the new value instantly instead of transitioning smoothly. This does not break the functionality. For browsers without `@property`, you can achieve transitions using a JavaScript `requestAnimationFrame` loop to interpolate the `--value`, though the native CSS transition is preferred for performance. - -See the [Spinner guide](../spinner/guide.md) for handling indeterminate loading states. diff --git a/guides/ui-components/spinner/demo.html b/guides/ui-components/spinner/demo.html index 24adf099d..2d09da0ab 100644 --- a/guides/ui-components/spinner/demo.html +++ b/guides/ui-components/spinner/demo.html @@ -80,10 +80,12 @@ } @layer component { - .loading-spinner { + .loading-spinner:has(> progress:indeterminate) { --size: 40px; --thickness: 4px; --spinner-color: #3b82f6; + --spinner-duration: 0.8s; + --spinner-timing: linear; position: relative; width: var(--size); @@ -101,16 +103,7 @@ black calc(50% - var(--thickness) + 0.5px) ); - animation: spinner-rotate 0.8s linear infinite; - } - - .loading-spinner--large { - --size: 64px; - --thickness: 6px; - } - - .loading-spinner--custom { - --spinner-color: #f43f5e; + animation: spinner-rotate var(--spinner-duration) var(--spinner-timing) infinite; } @keyframes spinner-rotate { @@ -123,6 +116,18 @@ } } } + @layer examples { + .loading-spinner--large { + --size: 64px; + --thickness: 6px; + } + + .loading-spinner--custom { + --spinner-color: #f43f5e; + --spinner-duration: 1.2s; + --spinner-timing: ease-in-out; + } + } diff --git a/guides/ui-components/spinner/guide.md b/guides/ui-components/spinner/guide.md index 0220c43c1..7f5d5a89f 100644 --- a/guides/ui-components/spinner/guide.md +++ b/guides/ui-components/spinner/guide.md @@ -17,7 +17,7 @@ This guide implements a spinner by: - Styling the component with `conic-gradient()` to create a visual "trail" and `mask-image` to hollow out the center into a ring. - Animating the spinner efficiently using CSS transforms and respecting `prefers-reduced-motion` to ensure a comfortable experience for all users. -See the [Progress Ring guide](../progress-ring/guide.md) for handling determinate tasks with a known duration. +See {{ GUIDE_REF("progress-ring") }} for handling determinate tasks with a known duration. ## Implementation @@ -39,13 +39,15 @@ Alternatively, you may choose to omit the `` element, and add the `sta #### Spinner Ring and Trail -The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image` is used to create the ring shape. +The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image` is used to create the ring shape. To ensure the loader is only shown when it is exposed to the accessibility tree, use `:has(> progress:indeterminate)`. ```css -.loading-spinner { +.loading-spinner:has(> progress:indeterminate) { --size: 40px; --thickness: 4px; --spinner-color: #3b82f6; + --spinner-duration: 0.8s; + --spinner-timing: linear; position: relative; width: var(--size); @@ -66,7 +68,7 @@ The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image ); /* Continuous rotation animation */ - animation: spinner-rotate 0.8s linear infinite; + animation: spinner-rotate var(--spinner-duration) var(--spinner-timing) infinite; } @keyframes spinner-rotate { From 9395a6230d7c8e1b0c0b245ca102bf823eb67380 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Mon, 24 Aug 2026 22:50:44 -0500 Subject: [PATCH 03/13] Preflight --- README.md | 61 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a6c12078b..2dcb5b4c4 100644 --- a/README.md +++ b/README.md @@ -87,41 +87,42 @@ _View an example:_ [the `navigation-drawer` guide](https://github.com/GoogleChro #### The full list
-103 modern web features +105 modern web features -### CSS & Layout (51 features) +### CSS & Layout (52 features) | | | | | :--- | :--- | :--- | -| [::backdrop](https://web-platform-dx.github.io/web-features-explorer/features/backdrop/) | [Custom highlights](https://web-platform-dx.github.io/web-features-explorer/features/highlight/) | [Scroll snap events](https://web-platform-dx.github.io/web-features-explorer/features/scroll-snap-events/) | -| [:has()](https://web-platform-dx.github.io/web-features-explorer/features/has/) | [field-sizing](https://web-platform-dx.github.io/web-features-explorer/features/field-sizing/) | [Scroll-driven animations](https://web-platform-dx.github.io/web-features-explorer/features/scroll-driven-animations/) | -| [:not()](https://web-platform-dx.github.io/web-features-explorer/features/not/) | [font-size-adjust](https://web-platform-dx.github.io/web-features-explorer/features/font-size-adjust/) | [scroll-initial-target](https://web-platform-dx.github.io/web-features-explorer/features/scroll-initial-target/) | -| [:user-valid and :user-invalid](https://web-platform-dx.github.io/web-features-explorer/features/user-pseudos/) | [image-set()](https://web-platform-dx.github.io/web-features-explorer/features/image-set/) | [scrollbar-color](https://web-platform-dx.github.io/web-features-explorer/features/scrollbar-color/) | -| [@function](https://web-platform-dx.github.io/web-features-explorer/features/function/) | [Individual transform properties](https://web-platform-dx.github.io/web-features-explorer/features/individual-transforms/) | [scrollbar-width](https://web-platform-dx.github.io/web-features-explorer/features/scrollbar-width/) | -| [@starting-style](https://web-platform-dx.github.io/web-features-explorer/features/starting-style/) | [interpolate-size](https://web-platform-dx.github.io/web-features-explorer/features/interpolate-size/) | [scrollend](https://web-platform-dx.github.io/web-features-explorer/features/scrollend/) | -| [accent-color](https://web-platform-dx.github.io/web-features-explorer/features/accent-color/) | [light-dark()](https://web-platform-dx.github.io/web-features-explorer/features/light-dark/) | [scrollIntoView()](https://web-platform-dx.github.io/web-features-explorer/features/scroll-into-view/) | -| [Active view transition](https://web-platform-dx.github.io/web-features-explorer/features/active-view-transition/) | [linear() easing](https://web-platform-dx.github.io/web-features-explorer/features/linear-easing/) | [sibling-count() and sibling-index()](https://web-platform-dx.github.io/web-features-explorer/features/sibling-count/) | -| [Anchor position container queries](https://web-platform-dx.github.io/web-features-explorer/features/container-anchor-position-queries/) | [Masks](https://web-platform-dx.github.io/web-features-explorer/features/masks/) | [text-box](https://web-platform-dx.github.io/web-features-explorer/features/text-box/) | -| [Anchor positioning](https://web-platform-dx.github.io/web-features-explorer/features/anchor-positioning/) | [overflow-clip-margin](https://web-platform-dx.github.io/web-features-explorer/features/overflow-clip-margin/) | [text-wrap](https://web-platform-dx.github.io/web-features-explorer/features/text-wrap/) | -| [calc-size()](https://web-platform-dx.github.io/web-features-explorer/features/calc-size/) | [overflow: clip](https://web-platform-dx.github.io/web-features-explorer/features/overflow-clip/) | [text-wrap: balance](https://web-platform-dx.github.io/web-features-explorer/features/text-wrap-balance/) | -| [color-scheme](https://web-platform-dx.github.io/web-features-explorer/features/color-scheme/) | [overlay](https://web-platform-dx.github.io/web-features-explorer/features/overlay/) | [text-wrap: pretty](https://web-platform-dx.github.io/web-features-explorer/features/text-wrap-pretty/) | -| [Container queries](https://web-platform-dx.github.io/web-features-explorer/features/container-queries/) | [overscroll-behavior](https://web-platform-dx.github.io/web-features-explorer/features/overscroll-behavior/) | [transition-behavior](https://web-platform-dx.github.io/web-features-explorer/features/transition-behavior/) | -| [Container scroll-state queries](https://web-platform-dx.github.io/web-features-explorer/features/container-scroll-state-queries/) | [prefers-color-scheme media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-color-scheme/) | [Trigonometric functions (CSS)](https://web-platform-dx.github.io/web-features-explorer/features/trig-functions/) | -| [Container style queries](https://web-platform-dx.github.io/web-features-explorer/features/container-style-queries/) | [prefers-contrast media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-contrast/) | [View transitions](https://web-platform-dx.github.io/web-features-explorer/features/view-transitions/) | -| [content-visibility](https://web-platform-dx.github.io/web-features-explorer/features/content-visibility/) | [prefers-reduced-motion media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-reduced-motion/) | [view-transition-class](https://web-platform-dx.github.io/web-features-explorer/features/view-transition-class/) | -| [Cross-document view transitions](https://web-platform-dx.github.io/web-features-explorer/features/cross-document-view-transitions/) | [Scroll snap](https://web-platform-dx.github.io/web-features-explorer/features/scroll-snap/) | [Web animations](https://web-platform-dx.github.io/web-features-explorer/features/web-animations/) | - -### HTML & DOM (20 features) +| [::backdrop](https://web-platform-dx.github.io/web-features-explorer/features/backdrop/) | [Custom highlights](https://web-platform-dx.github.io/web-features-explorer/features/highlight/) | [Scroll-driven animations](https://web-platform-dx.github.io/web-features-explorer/features/scroll-driven-animations/) | +| [:has()](https://web-platform-dx.github.io/web-features-explorer/features/has/) | [field-sizing](https://web-platform-dx.github.io/web-features-explorer/features/field-sizing/) | [scroll-initial-target](https://web-platform-dx.github.io/web-features-explorer/features/scroll-initial-target/) | +| [:not()](https://web-platform-dx.github.io/web-features-explorer/features/not/) | [font-size-adjust](https://web-platform-dx.github.io/web-features-explorer/features/font-size-adjust/) | [scrollbar-color](https://web-platform-dx.github.io/web-features-explorer/features/scrollbar-color/) | +| [:user-valid and :user-invalid](https://web-platform-dx.github.io/web-features-explorer/features/user-pseudos/) | [image-set()](https://web-platform-dx.github.io/web-features-explorer/features/image-set/) | [scrollbar-width](https://web-platform-dx.github.io/web-features-explorer/features/scrollbar-width/) | +| [@function](https://web-platform-dx.github.io/web-features-explorer/features/function/) | [Individual transform properties](https://web-platform-dx.github.io/web-features-explorer/features/individual-transforms/) | [scrollend](https://web-platform-dx.github.io/web-features-explorer/features/scrollend/) | +| [@starting-style](https://web-platform-dx.github.io/web-features-explorer/features/starting-style/) | [interpolate-size](https://web-platform-dx.github.io/web-features-explorer/features/interpolate-size/) | [scrollIntoView()](https://web-platform-dx.github.io/web-features-explorer/features/scroll-into-view/) | +| [accent-color](https://web-platform-dx.github.io/web-features-explorer/features/accent-color/) | [light-dark()](https://web-platform-dx.github.io/web-features-explorer/features/light-dark/) | [sibling-count() and sibling-index()](https://web-platform-dx.github.io/web-features-explorer/features/sibling-count/) | +| [Active view transition](https://web-platform-dx.github.io/web-features-explorer/features/active-view-transition/) | [linear() easing](https://web-platform-dx.github.io/web-features-explorer/features/linear-easing/) | [text-box](https://web-platform-dx.github.io/web-features-explorer/features/text-box/) | +| [Anchor position container queries](https://web-platform-dx.github.io/web-features-explorer/features/container-anchor-position-queries/) | [Masks](https://web-platform-dx.github.io/web-features-explorer/features/masks/) | [text-wrap](https://web-platform-dx.github.io/web-features-explorer/features/text-wrap/) | +| [Anchor positioning](https://web-platform-dx.github.io/web-features-explorer/features/anchor-positioning/) | [overflow-clip-margin](https://web-platform-dx.github.io/web-features-explorer/features/overflow-clip-margin/) | [text-wrap: balance](https://web-platform-dx.github.io/web-features-explorer/features/text-wrap-balance/) | +| [calc-size()](https://web-platform-dx.github.io/web-features-explorer/features/calc-size/) | [overflow: clip](https://web-platform-dx.github.io/web-features-explorer/features/overflow-clip/) | [text-wrap: pretty](https://web-platform-dx.github.io/web-features-explorer/features/text-wrap-pretty/) | +| [color-scheme](https://web-platform-dx.github.io/web-features-explorer/features/color-scheme/) | [overlay](https://web-platform-dx.github.io/web-features-explorer/features/overlay/) | [transition-behavior](https://web-platform-dx.github.io/web-features-explorer/features/transition-behavior/) | +| [Conic gradients](https://web-platform-dx.github.io/web-features-explorer/features/conic-gradients/) | [overscroll-behavior](https://web-platform-dx.github.io/web-features-explorer/features/overscroll-behavior/) | [Trigonometric functions (CSS)](https://web-platform-dx.github.io/web-features-explorer/features/trig-functions/) | +| [Container queries](https://web-platform-dx.github.io/web-features-explorer/features/container-queries/) | [prefers-color-scheme media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-color-scheme/) | [View transitions](https://web-platform-dx.github.io/web-features-explorer/features/view-transitions/) | +| [Container scroll-state queries](https://web-platform-dx.github.io/web-features-explorer/features/container-scroll-state-queries/) | [prefers-contrast media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-contrast/) | [view-transition-class](https://web-platform-dx.github.io/web-features-explorer/features/view-transition-class/) | +| [Container style queries](https://web-platform-dx.github.io/web-features-explorer/features/container-style-queries/) | [prefers-reduced-motion media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-reduced-motion/) | [Web animations](https://web-platform-dx.github.io/web-features-explorer/features/web-animations/) | +| [content-visibility](https://web-platform-dx.github.io/web-features-explorer/features/content-visibility/) | [Scroll snap](https://web-platform-dx.github.io/web-features-explorer/features/scroll-snap/) | | +| [Cross-document view transitions](https://web-platform-dx.github.io/web-features-explorer/features/cross-document-view-transitions/) | [Scroll snap events](https://web-platform-dx.github.io/web-features-explorer/features/scroll-snap-events/) | | + +### HTML & DOM (21 features) | | | | | :--- | :--- | :--- | -| [:autofill](https://web-platform-dx.github.io/web-features-explorer/features/autofill/) | [Customizable <select>](https://web-platform-dx.github.io/web-features-explorer/features/customizable-select/) | [Invoker commands](https://web-platform-dx.github.io/web-features-explorer/features/invoker-commands/) | -| [<details>](https://web-platform-dx.github.io/web-features-explorer/features/details/) | [Email, telephone, and URL <input> types](https://web-platform-dx.github.io/web-features-explorer/features/input-email-tel-url/) | [moveBefore()](https://web-platform-dx.github.io/web-features-explorer/features/move-before/) | -| [<dialog closedby>](https://web-platform-dx.github.io/web-features-explorer/features/dialog-closedby/) | [Fetch priority](https://web-platform-dx.github.io/web-features-explorer/features/fetch-priority/) | [MutationObserver](https://web-platform-dx.github.io/web-features-explorer/features/mutationobserver/) | -| [<dialog>](https://web-platform-dx.github.io/web-features-explorer/features/dialog/) | [hidden="until-found"](https://web-platform-dx.github.io/web-features-explorer/features/hidden-until-found/) | [Mutually exclusive <details> elements](https://web-platform-dx.github.io/web-features-explorer/features/details-name/) | -| [<link rel="expect">](https://web-platform-dx.github.io/web-features-explorer/features/link-rel-expect/) | [HTML in canvas](https://web-platform-dx.github.io/web-features-explorer/features/canvas-html/) | [Popover](https://web-platform-dx.github.io/web-features-explorer/features/popover/) | -| [<link rel="preload">](https://web-platform-dx.github.io/web-features-explorer/features/link-rel-preload/) | [inert](https://web-platform-dx.github.io/web-features-explorer/features/inert/) | [popover="hint"](https://web-platform-dx.github.io/web-features-explorer/features/popover-hint/) | -| [blocking="render"](https://web-platform-dx.github.io/web-features-explorer/features/blocking-render/) | [Interest invokers](https://web-platform-dx.github.io/web-features-explorer/features/interest-invokers/) | | +| [:autofill](https://web-platform-dx.github.io/web-features-explorer/features/autofill/) | [blocking="render"](https://web-platform-dx.github.io/web-features-explorer/features/blocking-render/) | [Interest invokers](https://web-platform-dx.github.io/web-features-explorer/features/interest-invokers/) | +| [<details>](https://web-platform-dx.github.io/web-features-explorer/features/details/) | [Customizable <select>](https://web-platform-dx.github.io/web-features-explorer/features/customizable-select/) | [Invoker commands](https://web-platform-dx.github.io/web-features-explorer/features/invoker-commands/) | +| [<dialog closedby>](https://web-platform-dx.github.io/web-features-explorer/features/dialog-closedby/) | [Email, telephone, and URL <input> types](https://web-platform-dx.github.io/web-features-explorer/features/input-email-tel-url/) | [moveBefore()](https://web-platform-dx.github.io/web-features-explorer/features/move-before/) | +| [<dialog>](https://web-platform-dx.github.io/web-features-explorer/features/dialog/) | [Fetch priority](https://web-platform-dx.github.io/web-features-explorer/features/fetch-priority/) | [MutationObserver](https://web-platform-dx.github.io/web-features-explorer/features/mutationobserver/) | +| [<link rel="expect">](https://web-platform-dx.github.io/web-features-explorer/features/link-rel-expect/) | [hidden="until-found"](https://web-platform-dx.github.io/web-features-explorer/features/hidden-until-found/) | [Mutually exclusive <details> elements](https://web-platform-dx.github.io/web-features-explorer/features/details-name/) | +| [<link rel="preload">](https://web-platform-dx.github.io/web-features-explorer/features/link-rel-preload/) | [HTML in canvas](https://web-platform-dx.github.io/web-features-explorer/features/canvas-html/) | [Popover](https://web-platform-dx.github.io/web-features-explorer/features/popover/) | +| [<progress>](https://web-platform-dx.github.io/web-features-explorer/features/progress/) | [inert](https://web-platform-dx.github.io/web-features-explorer/features/inert/) | [popover="hint"](https://web-platform-dx.github.io/web-features-explorer/features/popover-hint/) | ### JavaScript & APIs (32 features) @@ -142,7 +143,7 @@ _View an example:_ [the `navigation-drawer` guide](https://github.com/GoogleChro
-131 real-world developer use cases +133 real-world developer use cases

accessibility

@@ -288,6 +289,8 @@ _View an example:_ [the `navigation-drawer` guide](https://github.com/GoogleChro - **[navigation-drawer](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/navigation-drawer.md)**: Create a navigation drawer component that, when triggered from a menu button, slides in from the side overlayed on top of existing page content, and slides out when dismissed (by swiping away, tapping outside, or pressing escape). - **[persistent-app-tours](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/persistent-app-tours.md)**: Create persistent onboarding walkthroughs using tethered native overlays that stay open during user interaction. - **[persistent-toast-notifications](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/persistent-toast-notifications.md)**: Create non-intrusive toast and overlay notifications for persistent, stackable messaging and state communication. +- **[progress-ring](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/progress-ring.md)**: Build a progress ring component that visually represents the completion status of a task or process, with support for content in the center and brand-consistent styling. +- **[spinner](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/spinner.md)**: Build a loading spinner that communicates busy state to all users, respects reduced-motion preferences, and animates efficiently. - **[stack-drill-down](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/stack-drill-down.md)**: Build full-screen hierarchical navigation that lets users drill down into nested views and swipe or navigate back to return, with browser history kept in sync.

visual-design

From 54a5a88e7d9110071fc4caf8524f4dc43d2c1dfd Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Tue, 25 Aug 2026 08:57:46 -0500 Subject: [PATCH 04/13] Specificity and overrides dance --- guides/ui-components/spinner/demo.html | 12 ++++++++---- guides/ui-components/spinner/guide.md | 9 +++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/guides/ui-components/spinner/demo.html b/guides/ui-components/spinner/demo.html index 2d09da0ab..d00ea10f6 100644 --- a/guides/ui-components/spinner/demo.html +++ b/guides/ui-components/spinner/demo.html @@ -80,11 +80,12 @@ } @layer component { - .loading-spinner:has(> progress:indeterminate) { + .loading-spinner:where(.loading-spinner:has(> progress:indeterminate)) { --size: 40px; --thickness: 4px; --spinner-color: #3b82f6; --spinner-duration: 0.8s; + --_used-spinner-duration: var(--spinner-duration); --spinner-timing: linear; position: relative; @@ -103,16 +104,19 @@ black calc(50% - var(--thickness) + 0.5px) ); - animation: spinner-rotate var(--spinner-duration) var(--spinner-timing) infinite; + animation: spinner-rotate var(--_used-spinner-duration) + var(--spinner-timing) infinite; } @keyframes spinner-rotate { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } @media (prefers-reduced-motion: reduce) { .loading-spinner { - animation-duration: 3s; + --_used-spinner-duration: 3s; } } } diff --git a/guides/ui-components/spinner/guide.md b/guides/ui-components/spinner/guide.md index 7f5d5a89f..e3838dcce 100644 --- a/guides/ui-components/spinner/guide.md +++ b/guides/ui-components/spinner/guide.md @@ -39,14 +39,15 @@ Alternatively, you may choose to omit the `` element, and add the `sta #### Spinner Ring and Trail -The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image` is used to create the ring shape. To ensure the loader is only shown when it is exposed to the accessibility tree, use `:has(> progress:indeterminate)`. +The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image` is used to create the ring shape. To ensure the loader is only shown when it is exposed to the accessibility tree, use `:has(> progress:indeterminate)`. Wrap that in `.loading-spinner:where()` to allow users to override the custom variables with a simple `.loading-spinner` selector. ```css -.loading-spinner:has(> progress:indeterminate) { +.loading-spinner:where(.loading-spinner:has(> progress:indeterminate)) { --size: 40px; --thickness: 4px; --spinner-color: #3b82f6; --spinner-duration: 0.8s; + --_used-spinner-duration: var(--spinner-duration); --spinner-timing: linear; position: relative; @@ -91,14 +92,14 @@ The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image #### Respecting Motion Preferences -Users with motion sensitivities may find fast-spinning elements disorienting. Always respect the `prefers-reduced-motion` media query. +Users with motion sensitivities may find fast-spinning elements disorienting. Always respect the `prefers-reduced-motion` media query. Set the internal `--_used-spinner-duration` property to override the user's `--spinner-duration` value. ```css @media (prefers-reduced-motion: reduce) { .loading-spinner { /* Slow down the animation significantly rather than stopping it entirely, so the user still knows that the process is active. */ - animation-duration: 3s; + --_used-spinner-duration: 3s; } } ``` From 590ba37032843dffa1b93b0cd33134ebdcb603bc Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Thu, 27 Aug 2026 11:24:47 -0400 Subject: [PATCH 05/13] Spinner updates --- guides/ui-components/spinner/demo.html | 72 +++++++++++-------- guides/ui-components/spinner/guide.md | 97 ++++++++++++++++---------- 2 files changed, 102 insertions(+), 67 deletions(-) diff --git a/guides/ui-components/spinner/demo.html b/guides/ui-components/spinner/demo.html index d00ea10f6..79eb5ca09 100644 --- a/guides/ui-components/spinner/demo.html +++ b/guides/ui-components/spinner/demo.html @@ -5,19 +5,6 @@ Spinner Demo @@ -139,23 +155,17 @@
Default
-
- -
+
Large
-
- -
+
Custom
-
- -
+
diff --git a/guides/ui-components/spinner/guide.md b/guides/ui-components/spinner/guide.md index e3838dcce..fde31d9c0 100644 --- a/guides/ui-components/spinner/guide.md +++ b/guides/ui-components/spinner/guide.md @@ -24,72 +24,78 @@ See {{ GUIDE_REF("progress-ring") }} for handling determinate tasks with a known ### 1. Markup -We use a wrapper to hold the visual spinner. The `` element remains the semantic source of truth. Without a `value` attribute, it is implicitly indeterminate. +Use the native `` element as both the semantic source of truth and the visual component. Without a `value` attribute, it is implicitly indeterminate. ```html -
- - -
+ ``` -Alternatively, you may choose to omit the `` element, and add the `status` ARIA role to the `.loading-spinner` `
` element. - ### 2. Styles -#### Spinner Ring and Trail +#### Hiding Native UI +To style the `` element as a spinner, first hide the default browser styling for indeterminate progress bars. + +```css +/* Hide native bars */ +progress.loading-spinner:indeterminate::-webkit-progress-bar { + display: none; + background: none; +} +progress.loading-spinner:indeterminate::-webkit-progress-value { + display: none; + background: none; +} +progress.loading-spinner:indeterminate::-moz-progress-bar { + display: none; + background: none; +} +progress.loading-spinner:indeterminate::slider-fill { + display: none; + background: none; +} +``` -The spinner uses a `conic-gradient` to create a fading trail effect. `mask-image` is used to create the ring shape. To ensure the loader is only shown when it is exposed to the accessibility tree, use `:has(> progress:indeterminate)`. Wrap that in `.loading-spinner:where()` to allow users to override the custom variables with a simple `.loading-spinner` selector. +#### Spinner Ring and Trail +The spinner uses a `conic-gradient` to create a visual trail. Use `background-clip: border-area` to constrain the gradient to the border region. ```css -.loading-spinner:where(.loading-spinner:has(> progress:indeterminate)) { +progress.loading-spinner:indeterminate { --size: 40px; - --thickness: 4px; + --thickness: 2px; --spinner-color: #3b82f6; + --track-color: #e2e5e7; --spinner-duration: 0.8s; - --_used-spinner-duration: var(--spinner-duration); + --_used-spinner-duration: var(--spinner-duration); --spinner-timing: linear; - + position: relative; width: var(--size); height: var(--size); border-radius: 50%; + appearance: none; - /* Create a fading trail from the spinner color to transparent */ + /* Create the fading trail */ background: conic-gradient( from 0deg, - var(--spinner-color), - transparent 75% + var(--track-color) 25%, + var(--spinner-color) 25% ); - /* Hollow out the center to create a ring */ - mask-image: radial-gradient( - transparent calc(50% - var(--thickness)), - black calc(50% - var(--thickness) + 0.5px) - ); + background-clip: border-area; + border: var(--thickness) solid transparent; + background-origin: border-box; - /* Continuous rotation animation */ - animation: spinner-rotate var(--spinner-duration) var(--spinner-timing) infinite; + animation: spinner-rotate var(--_used-spinner-duration) + var(--spinner-timing) infinite; } @keyframes spinner-rotate { to { transform: rotate(360deg); } } - -/* Standard utility to visually hide elements while keeping them accessible */ -.visually-hidden:where(:not(:focus-within, :active)) { - position: absolute !important; - clip-path: inset(50%) !important; - overflow: hidden !important; - width: 1px !important; - height: 1px !important; - margin: -1px !important; - padding: 0 !important; - border: 0 !important; - white-space: nowrap !important; -} ``` +You can also use a `radial-gradient` to make rounded end caps. + #### Respecting Motion Preferences Users with motion sensitivities may find fast-spinning elements disorienting. Always respect the `prefers-reduced-motion` media query. Set the internal `--_used-spinner-duration` property to override the user's `--spinner-duration` value. @@ -104,3 +110,22 @@ Users with motion sensitivities may find fast-spinning elements disorienting. Al } ``` Alternatively, replace the spinner with a static text label for users with `prefers-reduced-motion` enabled. + +## Fallback strategies + +{{ FEATURE_FALLBACKS("background-clip-border-area") }} + +For browsers that don't yet support `background-clip: border-area`, fall back to a `mask-image` to hollow out the center. + +```css +/* Fallback: use mask-image to create the ring */ +@supports not (background-clip: border-area) { + --clip-boundary: calc(100% - var(--thickness)); + mask-image: radial-gradient( + farthest-side, + transparent var(--clip-boundary), + black var(--clip-boundary) + ); + border: 0; +} +``` \ No newline at end of file From 2c03acae1f242c6a7258f65d0629508d7a651c48 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Thu, 27 Aug 2026 13:19:45 -0400 Subject: [PATCH 06/13] Update progress ring, expectations --- guides/ui-components/progress-ring/demo.html | 102 ++++++++-------- .../progress-ring/expectations.md | 8 +- guides/ui-components/progress-ring/guide.md | 112 +++++++++++------- guides/ui-components/spinner/demo.html | 2 +- guides/ui-components/spinner/expectations.md | 6 +- 5 files changed, 131 insertions(+), 99 deletions(-) diff --git a/guides/ui-components/progress-ring/demo.html b/guides/ui-components/progress-ring/demo.html index 95d6ed27f..9f4bae705 100644 --- a/guides/ui-components/progress-ring/demo.html +++ b/guides/ui-components/progress-ring/demo.html @@ -5,19 +5,6 @@ Progress Ring Demo