Loading indicator
components specs/components/loading-indicator.kmd
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.
When this spec applies
Primary triggers
- Display short-running indeterminate progress
All triggers
- Action takes < 5s with no progress estimation
- Pull-to-refresh gesture
- Replace existing indeterminate circular spinner in Koder app
Specification body
Spec — Loading indicator
Companion:
progress-indicators.kmdcobre determinate + indeterminate progresso de longo prazo. Esta spec cobre loading short < 5s com shape morphing — pattern Expressive.
Princípios
- Short by contract — < 5s. Above that → Progress indicator (cross-link).
- Shape morph signature — cycles through
shape-library.kmdshapes; NOT a spinning circle. - Spring-driven — uses
motion.kmdR9 spring tokens; NOT duration-based. - Composable — primary consumer pra pull-to-refresh + button loading state.
R1 — When to use
| Use case | Use Loading indicator? |
|---|---|
| Click "Save", wait < 5s | YES |
| Pull-to-refresh feed | 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-to-refresh, button) | Cookie-4 → Burst → Flower → Cookie-4 |
| Compact (in chip, inline) | Cookie-4 → Cookie-7 only (faster) |
| 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-to-refresh, dialog |
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 — Pull-to-refresh integration
When hosted in pull-to-refresh (cross-link future pull-to-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 (
motion-effect-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-to-refresh: skip drag-distance-driven 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 ` |
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
- T1 Mount: render with size md → Cookie-4 shape visible.
- T2 Morph cycle: advance time 1.2s → cycles through Cookie-4 → Burst → Flower → Cookie-4.
- T3 Sizes: render sm/md/lg → diameter 24/40/64dp; stroke 2/3/4dp.
- T4 Color override: render with
errorrole → red. - T5 Pull-to-refresh integration: drag-threshold cross → morph begins; release → snap-back smooth.
- T6 Button loading: button enters loading → content replaced; bounds preserved.
- T7 Reduced-motion: animations disabled → opacity pulse only; aria-live announces.
- T8 A11y: aria-live "polite"; aria-label correct in active locale.
- N1 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
References
specs/themes/motion.kmdspecs/themes/shape-library.kmdspecs/components/progress-indicators.kmd