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.
coloralready takes any CSS color;'agentic'is one more value, typed'agentic' | (string & {})so autocomplete shows it. - Not an SVG gradient.
--pds-gradient-ai-activeis a CSSlinear-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. Below2xl, or whenisInline(a spinner inline with text, whatever its size), it is ignored. (The disc, the Agent Avatar in the ring, and theavatarVariantprop were removed.)- Tried and removed: a
discoverycolor; a gradient fill on the avatar's own bolt (overlay withmix-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
Spinnermarkup and sizes; the PDSpds-ai-active-shimmerutility. - Net-new: the
agenticcolor value,hasAvatar, and seven modifiers and parts on.pds-spinner. Seecustom.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
discorbolt. Ifboltwins it can become the default anddisccan go. - Does the shimmer on the
boltread 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
blackas 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
mergeClassesand a visually-hidden span in place of v2'smergeClassesandHiddenLabel(not exported by the published package).
Port notes
- This extends the existing
Spinner(src/components/loading-indicators/Spinner/). Copy the new CSS and theagenticbranch into the v2 file; no generator needed. - Swap the local
mergeClassesand the visually-hidden span for v2'smergeClassesandHiddenLabel.
Preview
npm run storybook, then Component Proposals/Spinner.