← All proposals

Spinner: agentic color variant

Extend the existing PDS v2 Spinner (not a new component).

Extends a PDS componentOpen in Storybook

proposals/Spinner/HANDOFF.md75 lines · View on GitHub

Handoff: Spinner (agentic color)

What and why

AI and agentic surfaces use the AI gradient, so their loading state needs a Spinner that matches the single-color default. This extends the existing PDS v2 Spinner (it is not a new component): color='agentic' paints the arc and track with the AI gradient (--pds-gradient-ai-active, the token pds-ai-active-border uses), and a new prop, hasAvatar, puts the Zappy bolt inside the ring for work an agent is doing (plain foreground color, from 2xl up).

  • Figma: Spinner page in PDS-Proposals (5 stories, light and dark, flat captures; sent 2026-10-16). Jira: none. Source: Erika's screenshot of the gradient and the default spinner at nine sizes.
  • Context: context.md

Exploration and decisions

  • Extend Spinner, not a new component. color already takes any CSS color; 'agentic' is one more value, typed 'agentic' | (string & {}) so autocomplete shows it.
  • Not an SVG gradient. --pds-gradient-ai-active is a CSS linear-gradient(), which an SVG stroke can't use, and copying its stops into <stop> would break the tokens-only rule. The agentic variant is CSS boxes with the gradient as background, clipped by an alpha mask in the ring and arc shapes.
  • Fixed gradient, spinning arc. The arc box rotates and its gradient layer counter-rotates, so the colors stay put while the arc moves. The track is the same gradient at 0.3 opacity.
  • hasAvatar (2026-10-21) puts the Zappy bolt inside the ring, in the plain foreground color: not the gradient, no shimmer, no disc. It is half the ring tall. It is a boolean, not a slot. Below 2xl, or when isInline (a spinner inline with text, whatever its size), it is ignored. (The disc, the Agent Avatar in the ring, and the avatarVariant prop were removed.)
  • Tried and removed: a discovery color; a gradient fill on the avatar's own bolt (overlay with mix-blend-mode).

API

Prop Type Default Purpose
className string none Additional class names.
color 'agentic' | string PDS default Arc color: any CSS color or var(--token), or 'agentic' for the AI gradient.
hasAvatar boolean false Show the Zappy bolt inside the ring (from 2xl up, not inline).
isInline boolean false Inline-block, no visible label (as v2).
label string 'Loading' Label, visible only with showLabel; always read by screen readers (as v2).
showLabel boolean false Show the label under the spinner (as v2).
size 's' | 'm' | 'l' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' '3xl' Spinner size (as v2).

Variants and states: all nine sizes, single color and agentic, with and without label, inline with text, hasAvatar (the plain bolt in the gradient ring).

Tokens used

  • --pds-color-foreground-default
  • --pds-gradient-ai-active
  • --pds-typography-ff-default
  • --pds-typography-fw-medium
  • --pds-typography-lh-m

Custom variables (see custom.md): --spinner-arc-mask, --spinner-bolt-mask, --spinner-color, --spinner-ring-mask.

Impact

From impact.md: nothing reaches existing components; the new classes are scoped to the Spinner block. These proposals import it, so they change with it: Button, IconButton (its isWorking state) and Timeline (the active status), and through them ChainOfThought, IdentityBlock, LabeledDivider, MenuButton, ModelSelector, Panel and SuggestionCard. Look at them in Storybook after any change here.

Reused vs net-new

  • Reused: the v2 Spinner markup and sizes; the PDS pds-ai-active-shimmer utility.
  • Net-new: the agentic color value, hasAvatar, and seven modifiers and parts on .pds-spinner. See custom.md.

Accessibility

  • The spinner keeps v2's behavior: the label is always in the DOM and read by screen readers, and is only visually hidden unless showLabel.
  • The bolt inside the ring is hidden from screen readers; the spinner's own label is what is announced.
  • The rotation and the shimmer stop under prefers-reduced-motion.
  • axe-core (the engine behind the Storybook a11y panel) found no violations on Default, All sizes, With label, Inline with text and With icon, in light and dark (before the disc and the gradient bolt were removed on 2026-10-21; run it again).

Decisions and open questions

  • Pick disc or bolt. If bolt wins it can become the default and disc can go.
  • Does the shimmer on the bolt read in dark mode? The PDS shimmer is a white band with no dark value.
  • Is the gap between the ring and the bolt right? The bolt is half the ring tall at every size.
  • Round arc ends come from the mask's stroke-linecap; confirm they look right at 2x in the smallest sizes.
  • Raw values. The mask data URIs contain black as a fill or stroke. It is alpha-only (never painted), so it is not a color use; a grep for raw colors will flag it.
  • The component uses a local mergeClasses and a visually-hidden span in place of v2's mergeClasses and HiddenLabel (not exported by the published package).

Port notes

  • This extends the existing Spinner (src/components/loading-indicators/Spinner/). Copy the new CSS and the agentic branch into the v2 file; no generator needed.
  • Swap the local mergeClasses and the visually-hidden span for v2's mergeClasses and HiddenLabel.

Preview

npm run storybook, then Component Proposals/Spinner.