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 + + + +
+
+ +
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..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 +
+ + +
+ 75% +
+
+``` + +### 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 + + + +
+
+
+
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..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