diff --git a/README.md b/README.md
index 40c78d25c..cff0227d1 100644
--- a/README.md
+++ b/README.md
@@ -87,42 +87,42 @@ _View an example:_ [the `navigation-drawer` guide](https://github.com/GoogleChro
#### The full list
-104 modern web features
+106 modern web features
-### CSS & Layout (53 features)
+### CSS & Layout (52 features)
| | | |
| :--- | :--- | :--- |
-| [::backdrop](https://web-platform-dx.github.io/web-features-explorer/features/backdrop/) | [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/) |
-| [:has()](https://web-platform-dx.github.io/web-features-explorer/features/has/) | [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/) |
-| [:not()](https://web-platform-dx.github.io/web-features-explorer/features/not/) | [image-set()](https://web-platform-dx.github.io/web-features-explorer/features/image-set/) | [scroll-target-group](https://web-platform-dx.github.io/web-features-explorer/features/scroll-target-group/) |
-| [:user-valid and :user-invalid](https://web-platform-dx.github.io/web-features-explorer/features/user-pseudos/) | [Individual transform properties](https://web-platform-dx.github.io/web-features-explorer/features/individual-transforms/) | [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/) | [interpolate-size](https://web-platform-dx.github.io/web-features-explorer/features/interpolate-size/) | [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/) | [light-dark()](https://web-platform-dx.github.io/web-features-explorer/features/light-dark/) | [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/) | [linear() easing](https://web-platform-dx.github.io/web-features-explorer/features/linear-easing/) | [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/) | [Masks](https://web-platform-dx.github.io/web-features-explorer/features/masks/) | [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/) | [overflow-clip-margin](https://web-platform-dx.github.io/web-features-explorer/features/overflow-clip-margin/) | [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](https://web-platform-dx.github.io/web-features-explorer/features/overflow-clip/) | [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/) | [overlay](https://web-platform-dx.github.io/web-features-explorer/features/overlay/) | [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/) | [overscroll-behavior](https://web-platform-dx.github.io/web-features-explorer/features/overscroll-behavior/) | [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/) | [prefers-color-scheme media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-color-scheme/) | [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-contrast media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-contrast/) | [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-reduced-motion media query](https://web-platform-dx.github.io/web-features-explorer/features/prefers-reduced-motion/) | [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/) | [Scroll marker target pseudo-classes](https://web-platform-dx.github.io/web-features-explorer/features/scroll-marker-targets/) | [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/) |
-| [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/) | |
-
-### 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 (33 features)
@@ -143,7 +143,7 @@ _View an example:_ [the `navigation-drawer` guide](https://github.com/GoogleChro
-132 real-world developer use cases
+134 real-world developer use cases
accessibility
@@ -290,7 +290,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.
-- **[scrollspy](https://github.com/GoogleChrome/modern-web-guidance/blob/main/skills/modern-web-guidance/guides/ui-components/scrollspy.md)**: Highlight the currently visible section of a page in a navigation menu
+- **[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
diff --git a/guides/ui-components/progress-ring/demo.html b/guides/ui-components/progress-ring/demo.html
new file mode 100644
index 000000000..d4c6fa8a7
--- /dev/null
+++ b/guides/ui-components/progress-ring/demo.html
@@ -0,0 +1,202 @@
+
+
+
+
+
+ Progress Ring Demo
+
+
+
+
+
+
+
+
+ Adjust Progress:
+
+
+
+
+
+
+
+
diff --git a/guides/ui-components/progress-ring/expectations.md b/guides/ui-components/progress-ring/expectations.md
new file mode 100644
index 000000000..67be4a03c
--- /dev/null
+++ b/guides/ui-components/progress-ring/expectations.md
@@ -0,0 +1,15 @@
+# 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 `background-clip: border-area`.
+- If `background-clip: border-area` is not supported, fall back to hollowing out the center of the ring with a radial gradient mask.
+- The `value` attribute on `` is used to drive the visual progress of the ring using `attr()`.
+- If using `attr()` on non-content properties is not supported, fallback to update the visual ring using a `MutationObserver` that monitors the `value` attribute and updates 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 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 3f9c01fce..15f4c6e6a 100644
--- a/guides/ui-components/progress-ring/guide.md
+++ b/guides/ui-components/progress-ring/guide.md
@@ -4,6 +4,183 @@ description: Build a progress ring component that visually represents the comple
web-feature-ids:
- progress
- conic-gradients
+ - masks
+ - registered-custom-properties
---
-
+# Progress ring
+
+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 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
+
+### 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
+
+```
+
+### 2. Styles
+
+#### Hiding Native UI
+To style the `` element as a progress ring, first hide the default browser styling for 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;
+}
+```
+
+#### Container and Ring
+
+The wrapper provides the positioning context. The `` element handles the visual gradient.
+
+```css
+.progress-ring-wrapper {
+ position: relative;
+ display: grid;
+ place-items: center;
+}
+
+progress.progress-ring {
+ --size: 150px;
+ --thickness: 16px;
+ --track-color: #f1f5f9;
+ --fill-color: #3b82f6;
+ --value: attr(value type());
+ width: var(--size);
+ height: var(--size);
+
+ transition: --value 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ border-radius: 50%;
+
+ background: conic-gradient(
+ var(--fill-color) calc(var(--value) * 1%),
+ var(--track-color) 0
+ );
+
+ /* MANDATORY: Clip the background to the border-area. */
+ background-clip: border-area;
+ border: var(--thickness) solid transparent;
+ background-origin: border-box;
+}
+
+.progress-ring-content {
+ /* Positioned in the center of the wrapper */
+ position: absolute;
+}
+```
+
+You can also use a `radial-gradient` to make rounded end caps.
+
+#### Enable smooth transitions with `@property`
+
+To animate the progress ring smoothly when the value changes, register `--value` as a numeric custom property.
+
+Users with motion sensitivities may find the transition between values disorienting. Respect the `prefers-reduced-motion` media query by having a 0 second (immediate) duration by default, and setting a longer time for users with no preference.
+
+
+```css
+@property --value {
+ syntax: '';
+ inherits: true;
+ initial-value: 0;
+}
+
+progress.progress-ring {
+ transition: --value 0s ease-in-out;
+ @media (prefers-reduced-motion: no-preference) {
+ transition-duration: 0.4s;
+ }
+}
+
+```
+
+### 3. Progress Updates
+
+Update the `value` attribute on the `` element and the text content of `.progress-ring-content` (if displaying a percentage) whenever the value changes.
+
+### 4. Optional Success State
+
+You can use the CSS attribute selector 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.progress-ring[value="100"] {
+ --fill-color: #10b981;
+}
+```
+
+## Fallback strategies
+
+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. In most cases this is fine, and does not break any functionality.
+
+If the transition is absolutely necessary, you can check for `@property` support and use a `requestAnimationFrame()` loop to interpolate `--value` in older browsers.
+
+{{ 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 of the `` element.
+
+```css
+@supports not (background-clip: border-area) {
+ mask-image: radial-gradient(
+ transparent calc(50% - var(--thickness)),
+ black calc(50% - var(--thickness) + 0.5px)
+ );
+ border: 0;
+}
+```
+
+{{ FEATURE_FALLBACKS("attr") }}
+
+For browsers that don't support the `attr()` CSS function for any property, use a `MutationObserver` to automatically sync the `value` attribute to the `--value` custom property.
+
+```js
+if (!CSS.supports("width: attr(value type())")) {
+ const observer = new MutationObserver(() => {
+ progress.style.setProperty("--value", progress.getAttribute("value"));
+ });
+ observer.observe(progress, {
+ attributes: true,
+ attributeFilter: ["value"],
+ });
+}
+```
\ No newline at end of file
diff --git a/guides/ui-components/spinner/demo.html b/guides/ui-components/spinner/demo.html
new file mode 100644
index 000000000..68434da6c
--- /dev/null
+++ b/guides/ui-components/spinner/demo.html
@@ -0,0 +1,214 @@
+
+
+
+
+
+ Spinner Demo
+
+
+
+
+
+
diff --git a/guides/ui-components/spinner/expectations.md b/guides/ui-components/spinner/expectations.md
new file mode 100644
index 000000000..be5d36353
--- /dev/null
+++ b/guides/ui-components/spinner/expectations.md
@@ -0,0 +1,13 @@
+# 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 component is visible on the page.
+- The visual spinner is implemented using a CSS `conic-gradient` with dynamic angles driven by CSS custom properties.
+- The spinner uses a dual-animation approach: a continuous rotation (`progress-spin`) and a "dash" effect (`progress-dash`) that varies the length of the arc.
+- The dash animation is implemented using `@property` to register and animate custom properties (`--arc-start`, `--arc-end`), and is conditionally included using a `--progress-dash-animation` property with `syntax: "*"`.
+- If `@property` is not supported, the dash animation fails gracefully in CSS (no JS required), falling back to a simple rotating ring.
+- The center of the progress ring is transparent or "hollowed out" using `background-clip: border-area`.
+- If `background-clip: border-area` is not supported, fall back to hollowing out the center of the ring with a radial gradient mask.
+- The spinner respects `prefers-reduced-motion` by significantly slowing down the animation.
diff --git a/guides/ui-components/spinner/guide.md b/guides/ui-components/spinner/guide.md
index 9a7ce2e56..274032d03 100644
--- a/guides/ui-components/spinner/guide.md
+++ b/guides/ui-components/spinner/guide.md
@@ -7,4 +7,169 @@ web-feature-ids:
- progress
---
-
+# Loading spinner
+
+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 `background-clip: border-area` 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 {{ GUIDE_REF("progress-ring") }} for handling determinate tasks with a known duration.
+
+
+## Implementation
+
+### 1. Markup
+
+Use the native `` element as both the semantic source of truth and the visual component. Without a `value` attribute, it is implicitly indeterminate.
+
+```html
+
+```
+
+### 2. Styles
+
+#### 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;
+}
+```
+
+#### Spinner Ring and Trail
+The spinner uses a `conic-gradient` to create a visual trail.
+
+```css
+@property --arc-start {
+ syntax: "";
+ inherits: false;
+ initial-value: 0deg;
+}
+@property --arc-end {
+ syntax: "";
+ inherits: false;
+ initial-value: 0deg;
+}
+/* Use a custom property to conditionally include the dash animation */
+@property --progress-dash-animation {
+ syntax: "*";
+ inherits: false;
+ initial-value: , progress-dash 3s ease-in-out infinite;
+}
+
+progress.loading-spinner:indeterminate {
+ --_from: calc(90deg + var(--arc-start, 0deg));
+ --_to: calc(90deg + var(--arc-end, 158deg));
+ --size: 40px;
+ --thickness: 2px;
+ --spinner-color: #3b82f6;
+ --track-color: #e2e5e7;
+ --spinner-duration: 1.5s;
+ --_used-spinner-duration: var(--spinner-duration);
+ --spinner-timing: linear;
+
+ position: relative;
+ width: var(--size);
+ height: var(--size);
+ border-radius: 50%;
+ appearance: none;
+
+ /* Create the fading trail with dynamic angles */
+ background: conic-gradient(
+ from var(--_from),
+ var(--spinner-color) calc(var(--_to) - var(--_from)),
+ transparent 0
+ )
+ var(--track-color);
+
+ @supports (background-clip: border-area) {
+ background-clip: border-area;
+ border: var(--thickness) solid transparent;
+ background-origin: border-box;
+ }
+
+ /* ... fallback for background-clip: border-area ... */
+
+ /* The dash animation is only included if @property is supported */
+ animation:
+ progress-spin var(--_used-spinner-duration) linear infinite
+ var(--progress-dash-animation, );
+
+ @keyframes progress-spin {
+ to {
+ rotate: 1turn;
+ }
+ }
+
+ @keyframes progress-dash {
+ from {
+ --arc-start: 0deg;
+ --arc-end: 3deg;
+ }
+ 50% {
+ --arc-start: 100deg;
+ --arc-end: 358deg;
+ }
+ to {
+ --arc-start: 360deg;
+ --arc-end: 363deg;
+ }
+ }
+}
+```
+
+#### 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.
+
+```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. */
+ --_used-spinner-duration: 6s;
+ }
+}
+```
+
+## Fallback strategies
+
+{{ FEATURE_FALLBACKS("registered-custom-properties") }}
+
+If `@property` is supported, the dash animation is automatically included via the `--progress-dash-animation` property's `initial-value`. In browsers without `@property` support, the property registration is ignored, and the animation falls back to a simple rotation. No JavaScript is required for this fallback.
+
+{{ 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