Loading indicator
Material 3 Expressive Loading indicator — distinct from Progress indicators. Morphing shape (Cookie → Burst → Flower → Cookie cycle) via spring physics. Used for actions < 5s; replaces most indeterminate circular spinners. Integrates with pull-to-refresh.
Spec — Loading indicator
Companion:
progress-indicators.kmdcobre determinate + indeterminate progresso de longo prazo. Esta spec cobre *oading short < 5s*com shape morphing — pattern Expressive.
Princípios
- *hort by contract*— < 5s. Above that → Progress indicator (cross-link).
- *hape morph signature*— cycles through
shape-library.kmdshapes; NOT a spinning circle. - *pring
driven*— usesbased.motion.kmdR9 spring tokens; NOT duration - *omposable*— primary consumer pra pull
torefresh + button loading state.
R1 — When to use
| Use case | Use Loading indicator? |
|---|---|
| Click "Save", wait < 5s | YES |
| Pull |
YES |
| Long upload (> 5s, progress known) | NO — use Progress (determinate) |
| Indeterminate > 5s, no progress | NO — use Progress (indeterminate) |
| Background sync (invisible to user) | NO — toast/snackbar only |
| Pre-action loading (button) | YES — replaces button content briefly |
Decision tree formalized:
Action duration estimable?
├── YES: use Progress (determinate, %)
└── NO:
├── < 5s expected → Loading indicator (this spec)
└── ≥ 5s expected → Progress (indeterminate)R2 — Shape morph cycle
Default morph sequence (per shape-library.kmd):
Cookie-4 → Cookie-7 → Burst → Flower → Cookie-4 → ...Each transition driven by motion-spatial-default spring (per motion.kmd R9.1). Cycle duration: ~1.2s for full loop (3 morphs × 400ms each).
Configurable per consumer:
| Context | Cycle |
|---|---|
| Default (pull |
Cookie |
| Compact (in chip, inline) | Cookie |
| Hero (full-screen) | Full library traversal (longer perceived) |
R3 — Sizes + colors
| Size token | Diameter (dp) | Stroke width | Use |
|---|---|---|---|
sm |
24 | 2 | Inline (button, chip) |
md |
40 | 3 | Pull |
lg |
64 | 4 | Hero/empty state |
Color: primary color role per themes/color-roles.kmd. Per-state override:
| State | Color override |
|---|---|
| Default | primary |
| Disabled context | text-muted |
| Error retry context | error |
R4 — Pulltorefresh integration
When hosted in pulltorefresh (crosslink future `pullto-refresh.kmd` #075):
- Drag distance < threshold (40dp): static
Cookie-4(smaller scale 0.6). - Drag distance ≥ threshold: morph begins (continuous).
- Release < threshold: snap-back spring; indicator disappears.
- Release ≥ threshold: refresh triggers; indicator continues cycling until completion.
R5 — Button loading state
When button enters loading (per buttons.kmd):
- Replace button content (text + icon) with Loading indicator (size sm).
- Maintain button bounds (no width jump).
- Button disabled (no double-tap).
- On completion: replace back to original content via cross
fade (`motioneffect-fast`).
R6 — Surface bindings
| Surface | API |
|---|---|
| Flutter | KoderLoadingIndicator({size, color}) em koder_kit/lib/src/ai/ (futuro) OR koder_kit/lib/src/loading/ |
| Web | <koder-loading-indicator size="md"> em koder_web_kit |
| Compose Android | KoderLoadingIndicator via koder-design-compose (futuro) |
| SwiftUI iOS | idem via koder-design-swift (futuro) |
| CLI / TUI | n/a (terminal usa spinner ASCII canônico) |
R7 — Reduced-motion
prefers-reduced-motion: reduce:
- Morph cycle disabled.
- Static shape (default
Cookie-4) with opacity pulse 0.5↔1.0 over 1.2s (visual "still active" signal). - Pull
torefresh: skip dragdistancedriven morph; show static indicator on threshold cross.
R8 — Acessibilidade
- Container:
role="status" aria-live="polite" aria-label="Loading"(i18n). - After done: live region announces "Done" (configurable per context).
- Cursor: pointer if interactive (rare; cancel button siblings).
- Visual ≥ 24dp minimum (sm).
R9 — i18n
| Key | en-US | pt-BR |
|---|---|---|
loading.label.default |
"Loading" | "Carregando" |
loading.label.refreshing |
"Refreshing" | "Atualizando" |
loading.label.saving |
"Saving" | "Salvando" |
loading.label.done |
"Done" | "Concluído" |
R10 — Per-preset variation
| Preset | Loading indicator behavior | |
|---|---|---|
material3 / material_expressive |
Default (shape morph) | |
material2 |
Circular spinner (no morph) — fallback to old Material progress | |
terminal_classic |
ASCII spinner ` | /-` cycle |
brutalist |
Square block 100% color toggle (no curves) | |
cyberpunk_neon |
Default + glow halo | |
minimalist_mono |
Single thin line scaling 0% → 100% width | |
glassmorphism |
Default + backdrop blur ring |
T-suite
- *1*Mount: render with size md → Cookie-4 shape visible.
- *2*Morph cycle: advance time 1.2s → cycles through Cookie
4 → Burst → Flower → Cookie4. - *3*Sizes: render smmdlg → diameter 244064dp; stroke 234dp.
- *4*Color override: render with
errorrole → red. - *5*Pull
torefresh integration: dragthreshold cross → morph begins; release → snapback smooth. - *6*Button loading: button enters loading → content replaced; bounds preserved.
- *7*Reduced
motion: animations disabled → opacity pulse only; arialive announces. - *8*A11y: aria
live "polite"; arialabel correct in active locale. - *1*Long action (>5s): policy violation lint should warn — Loading indicator NOT for > 5s use cases.
Cross-link
- Sibling:
progress-indicators.kmd - Drivers:
motion.kmdR9,shape-library.kmdR2 - Consumers:
buttons.kmd(button loading state), futurepull-to-refresh.kmd(#075) - Color:
color-roles.kmd - Refs: M3 Loading indicator https://m3.material.io/components/loading-indicator/overview