Skip to content

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

All triggers

Specification body

Spec — Loading indicator

Companion: progress-indicators.kmd cobre determinate + indeterminate progresso de longo prazo. Esta spec cobre loading short < 5s com shape morphing — pattern Expressive.

Princípios

  1. Short by contract — < 5s. Above that → Progress indicator (cross-link).
  2. Shape morph signature — cycles through shape-library.kmd shapes; NOT a spinning circle.
  3. Spring-driven — uses motion.kmd R9 spring tokens; NOT duration-based.
  4. Composable — primary consumer pra pull-to-refresh + button loading state.

R1 — When to use

Use caseUse Loading indicator?
Click "Save", wait < 5sYES
Pull-to-refresh feedYES
Long upload (> 5s, progress known)NO — use Progress (determinate)
Indeterminate > 5s, no progressNO — 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:

ContextCycle
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 tokenDiameter (dp)Stroke widthUse
sm242Inline (button, chip)
md403Pull-to-refresh, dialog
lg644Hero/empty state

Color: primary color role per themes/color-roles.kmd. Per-state override:

StateColor override
Defaultprimary
Disabled contexttext-muted
Error retry contexterror

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

SurfaceAPI
FlutterKoderLoadingIndicator({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 AndroidKoderLoadingIndicator via koder-design-compose (futuro)
SwiftUI iOSidem via koder-design-swift (futuro)
CLI / TUIn/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

Keyen-USpt-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

PresetLoading indicator behavior
material3 / material_expressiveDefault (shape morph)
material2Circular spinner (no morph) — fallback to old Material progress
terminal_classicASCII spinner `
brutalistSquare block 100% color toggle (no curves)
cyberpunk_neonDefault + glow halo
minimalist_monoSingle thin line scaling 0% → 100% width
glassmorphismDefault + 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 error role → 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.

Requirements (testable)

Requirement: Short-by-contract usage {#req-loading-short-by-contract}

The Loading indicator SHALL be used only for indeterminate actions expected to complete in under 5 seconds; for actions with a known estimable duration, or indeterminate actions expected to take 5 seconds or longer, a Progress indicator SHALL be used instead and the Loading indicator SHALL NOT be selected.

Scenario: Short indeterminate action selects the Loading indicator

  • GIVEN an action with no progress estimation expected to finish in under 5 seconds (e.g. click "Save")
  • WHEN the component is chosen for the loading state
  • THEN the Loading indicator is rendered

Scenario: Long or estimable action rejects the Loading indicator

  • GIVEN an action expected to take 5 seconds or longer, or whose duration is estimable as a percentage
  • WHEN a developer wires a Loading indicator to it
  • THEN a policy-violation lint warns that the Loading indicator is not for >= 5s use cases
  • AND the guidance directs the developer to a Progress indicator (determinate or indeterminate)

Requirement: Shape-morph signature {#req-loading-shape-morph}

The Loading indicator SHALL render as a morphing shape that cycles through the shape-library sequence Cookie-4 → Burst → Flower → Cookie-4 driven by the motion-spatial-default spring, and SHALL NOT render as a spinning circle in the default (Material 3 Expressive) preset.

Scenario: Initial mount shows the base shape

  • GIVEN a Loading indicator with size md and the default preset
  • WHEN the component mounts
  • THEN the Cookie-4 shape is visible
  • AND no rotating circular spinner is drawn

Scenario: Cycle morphs through the shape sequence

  • GIVEN a mounted Loading indicator in the default preset
  • WHEN animation time advances by the full ~1.2s loop
  • THEN the rendered shape transitions Cookie-4 → Burst → Flower → Cookie-4
  • AND each transition is spring-driven (motion-spatial-default), not duration-based

Requirement: Size and color tokens {#req-loading-size-color}

The Loading indicator SHALL honor the sm/md/lg size tokens with diameters 24/40/64 dp and stroke widths 2/3/4 dp respectively, and SHALL paint with the primary color role by default while applying the mapped override (text-muted disabled, error retry) when a state context is supplied.

Scenario: Each size token maps to its diameter and stroke

  • GIVEN a Loading indicator
  • WHEN it is rendered with size sm, then md, then lg
  • THEN the diameters measure 24 dp, 40 dp, and 64 dp respectively
  • AND the stroke widths measure 2 dp, 3 dp, and 4 dp respectively

Scenario: Error-retry context overrides the color role

  • GIVEN a Loading indicator placed in an error-retry context
  • WHEN it renders
  • THEN it paints with the error color role instead of primary

Requirement: Button loading state {#req-loading-button-state}

When a button enters its loading state, it SHALL replace its content (text and icon) with a size-sm Loading indicator while preserving the button's original bounds, SHALL disable the button to prevent double activation, and on completion SHALL cross-fade back to the original content using motion-effect-fast.

Scenario: Entering loading replaces content without resizing

  • GIVEN a button with text and icon at a fixed width
  • WHEN the button enters its loading state
  • THEN its content is replaced by a size-sm Loading indicator
  • AND the button's width and height are unchanged
  • AND the button is disabled so a second tap is ignored

Scenario: Completion restores original content

  • GIVEN a button showing the Loading indicator
  • WHEN the action completes
  • THEN the original text and icon are restored via a motion-effect-fast cross-fade
  • AND the button becomes enabled again

Requirement: Pull-to-refresh drag behavior {#req-loading-pull-to-refresh}

When hosted in a pull-to-refresh gesture, the Loading indicator SHALL remain a static scaled-down Cookie-4 below the drag threshold, SHALL begin its continuous morph once the drag distance reaches the threshold, and on release SHALL snap back and disappear if under threshold or continue cycling until refresh completion if at or above threshold.

Scenario: Drag past threshold starts the morph

  • GIVEN a pull-to-refresh host with a 40 dp threshold
  • WHEN the drag distance is below 40 dp
  • THEN a static Cookie-4 is shown at 0.6 scale
  • WHEN the drag distance reaches or exceeds 40 dp
  • THEN the shape morph begins and animates continuously

Scenario: Release below threshold snaps back

  • GIVEN an in-progress pull-to-refresh drag below threshold
  • WHEN the gesture is released
  • THEN a snap-back spring plays and the indicator disappears
  • AND no refresh is triggered

Requirement: Reduced motion and accessibility {#req-loading-a11y}

The Loading indicator SHALL expose an accessible live status (role="status", aria-live="polite", localized aria-label) with a minimum visual size of 24 dp, and under prefers-reduced-motion: reduce it SHALL disable the shape-morph cycle and instead show a static Cookie-4 with a 0.5↔1.0 opacity pulse over 1.2 s.

Scenario: Active indicator announces politely with a localized label

  • GIVEN a Loading indicator active in the current locale
  • WHEN it is mounted
  • THEN its container carries role="status" and aria-live="polite"
  • AND its aria-label reads the loading label for the active locale ("Loading" in en-US, "Carregando" in pt-BR)
  • AND its rendered size is at least 24 dp

Scenario: Reduced motion replaces morph with an opacity pulse

  • GIVEN the environment reports prefers-reduced-motion: reduce
  • WHEN the Loading indicator is active
  • THEN the shape-morph cycle is not animated
  • AND a static Cookie-4 pulses opacity between 0.5 and 1.0 over 1.2 s

References