Theme
Version
GitHub PyPI Discord
On this page

Spinner

Use CSpinner for compact activity whose duration is unknown. It renders one indeterminate progressbar, works before JavaScript loads, and always requires an accessible task label.

Spinner at a glance

Spinner at a glance
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class SpinnerAtAGlance(Component):
    template = """
      <section class="spinner-glance">
        <div class="spinner-glance__sky" aria-hidden="true">โœฆ ยท โœง ยท โœฆ</div>
        <c-CGroup justify="center">
          <c-CSpinner label="Calibrating deep-sky camera" size="lg" />
          <div><h2>Calibrating the camera</h2><p>Reading dark frames from the observatory sensor.</p></div>
        </c-CGroup>
      </section>
    """
    css = """
      :where(.spinner-glance) {
        display: grid;
        gap: 1rem;
        max-inline-size: 34rem;
        padding: 1.25rem;
        border: 1px solid light-dark(#b8b8dd, #4b4a78);
        border-radius: 0.9rem;
        background: light-dark(#f7f6ff, #17172a);
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.spinner-glance__sky) {
        color: light-dark(#5b4bb7, #c4b5fd);
        font-size: 1.3rem;
        letter-spacing: 0.6rem;
        text-align: center;
      }

      :where(.spinner-glance h2, .spinner-glance p) {
        margin: 0;
      }

      :where(.spinner-glance p) {
        margin-block-start: 0.25rem;
        color: light-dark(#55546f, #c6c4de);
        font-size: 0.8rem;
      }
    """


preview = SpinnerAtAGlance()

preview  # noqa: B018

Show active work

Pass a concise label that identifies the active task. Spinner does not display the label, so pair it with visible text when users need the same context.

Show basic Spinners
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class BasicSpinners(Component):
    template = """
      <c-CGroup class_="spinner-basic" gap="lg">
        <c-CSpinner label="Loading lunar atlas" />
        <c-CSpinner label="Aligning telescope mount" intent="success" />
        <c-CSpinner label="Reconnecting weather station" intent="warn" />
      </c-CGroup>
    """
    css = """
      :where(.spinner-basic) {
        padding: 1.25rem;
        color: CanvasText;
      }
    """


preview = BasicSpinners()

preview  # noqa: B018
<c-CSpinner label="Loading star catalog" />

Choose a palette

Intent changes the ring color. Keep status meaning in surrounding text rather than color alone.

Compare Spinner intents
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class SpinnerIntents(Component):
    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <c-CGroup class_="spinner-intents" gap="lg" wrap>
        <c-for each="intent in intents">
          <c-CStack c-attrs="{'data-spinner-intent-example': intent}" align="center" gap="xs">
            <c-CSpinner c-label="f'{intent} observatory task'" c-intent="intent" />
            <span>{{ intent }}</span>
          </c-CStack>
        </c-for>
      </c-CGroup>
    """

    def template_data(
        self,
        kwargs: Kwargs,  # noqa: ARG002
        slots: Slots,  # noqa: ARG002
    ) -> dict[str, object]:
        return {"intents": ("neutral", "primary", "success", "warn", "danger")}

    css = """
      :where(.spinner-intents) {
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.spinner-intents span) {
        font-size: 0.72rem;
      }
    """


preview = SpinnerIntents()

preview  # noqa: B018

Choose a size

Use sm, md, or lg. Public CSS variables can set a one-off diameter or thickness.

Compare Spinner sizes
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class SpinnerSizes(Component):
    class Kwargs:
        pass

    class Slots:
        pass

    template = """
      <c-CGroup class_="spinner-sizes" gap="lg" align="center">
        <c-for each="size in sizes">
          <c-CStack align="center" gap="xs">
            <c-CSpinner c-label="f'{size} star-map load'" c-size="size" />
            <span>{{ size }}</span>
          </c-CStack>
        </c-for>
      </c-CGroup>
    """

    def template_data(
        self,
        kwargs: Kwargs,  # noqa: ARG002
        slots: Slots,  # noqa: ARG002
    ) -> dict[str, object]:
        return {"sizes": ("sm", "md", "lg")}

    css = """
      :where(.spinner-sizes) {
        min-block-size: 4rem;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.spinner-sizes span) {
        font-size: 0.72rem;
      }
    """


preview = SpinnerSizes()

preview  # noqa: B018

Pair Spinner with text

Spinner is inline-sized and works beside concise status text. It never adds a focus stop or changes surrounding controls.

Compose inline activity
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class InlineSpinner(Component):
    template = """
      <section class="spinner-inline">
        <c-CGroup gap="sm">
          <c-CSpinner label="Indexing nebula spectra" size="sm" />
          <span>Indexing nebula spectra</span>
        </c-CGroup>
        <p>The rest of the observing log remains readable while the index catches up.</p>
      </section>
    """
    css = """
      :where(.spinner-inline) {
        max-inline-size: 34rem;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.spinner-inline p) {
        margin-block-end: 0;
        color: light-dark(#57566f, #c8c6df);
        font-size: 0.8rem;
      }
    """


preview = InlineSpinner()

preview  # noqa: B018

Control presentation in the browser

Client inputs are passed through $c-props="{...}". They can update label, intent, and size; omission returns to the server fallback.

Control Spinner in the browser
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class ControlledSpinner(Component):
    template = """
      <section
        class="spinner-controlled"
        x-init="Alpine.store('spinnerControls', {intent: 'primary', size: 'md'})"
      >
        <c-CGroup>
          <c-CSpinner
            label="Refreshing orbital catalog"
            $c-props="{
              intent: $store.spinnerControls.intent,
              size: $store.spinnerControls.size,
            }"
          />
          <span>Refreshing orbital catalog</span>
        </c-CGroup>
        <c-CGroup wrap>
          <label>
            Intent
            <select x-model="$store.spinnerControls.intent">
              <option>primary</option><option>success</option>
              <option>warn</option><option>danger</option>
            </select>
          </label>
          <label>
            Size
            <select x-model="$store.spinnerControls.size">
              <option>sm</option><option>md</option><option>lg</option>
            </select>
          </label>
        </c-CGroup>
      </section>
    """
    css = """
      :where(.spinner-controlled) {
        display: grid;
        gap: 1rem;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.spinner-controlled label) {
        display: grid;
        gap: 0.3rem;
        font-size: 0.75rem;
      }
    """


preview = ControlledSpinner()

preview  # noqa: B018

Describe a busy region

The region owner sets aria-busy, controls Spinner presence, and clears busy state when work completes. Spinner does not mutate another element.

Connect Spinner to a busy region
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class SpinnerBusyRegion(Component):
    template = """
      <section class="spinner-busy" aria-busy="true" aria-describedby="star-chart-status">
        <c-CGroup>
          <c-CSpinner label="Updating star chart" c-attrs="{'id': 'star-chart-status'}" />
          <strong>Updating the star chart</strong>
        </c-CGroup>
        <div class="spinner-busy__chart" aria-hidden="true">
          โœฆ&nbsp;&nbsp;&nbsp;ยท&nbsp;&nbsp;โœง<br />
          &nbsp;&nbsp;ยท&nbsp;&nbsp;&nbsp;โœฆ&nbsp;&nbsp;ยท
        </div>
      </section>
    """
    css = """
      :where(.spinner-busy) {
        display: grid;
        gap: 1rem;
        max-inline-size: 30rem;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.spinner-busy__chart) {
        min-block-size: 5rem;
        padding: 1rem;
        border-radius: 0.7rem;
        background: light-dark(#ecebff, #1d1d35);
        color: light-dark(#5148a0, #c4b5fd);
        letter-spacing: 0.7rem;
        line-height: 2;
      }
    """


preview = SpinnerBusyRegion()

preview  # noqa: B018

Avoid flashes for brief work

Delay Spinner in application state when a task normally finishes immediately. The application also owns any minimum-visible duration.

Delay brief activity feedback
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class DelayedSpinner(Component):
    template = """
      <section class="spinner-delayed" x-data="{visible: false}">
        <button type="button" @click="visible = !visible">Toggle long-running observation</button>
        <div x-show="visible" class="spinner-delayed__status">
          <c-CSpinner label="Waiting for long exposure" size="sm" />
          <span>Waiting for the long exposure</span>
        </div>
        <p>Real applications show this only after their chosen delay.</p>
      </section>
    """
    css = """
      :where(.spinner-delayed) {
        display: grid;
        gap: 0.75rem;
        color: CanvasText;
        font-family: ui-sans-serif, system-ui, sans-serif;
      }

      :where(.spinner-delayed__status) {
        display: flex;
        align-items: center;
        gap: 0.5rem;
      }

      :where(.spinner-delayed p) {
        margin: 0;
        color: light-dark(#57566f, #c8c6df);
        font-size: 0.78rem;
      }
    """


preview = DelayedSpinner()

preview  # noqa: B018

Customize Spinner

Override public color, track, diameter, thickness, and duration variables on an ancestor or one Spinner root.

Customize Spinner with public CSS
Show code
import citry_ui
from citry import Component, citry

citry.register_library(citry_ui)


class SpinnerCustomization(Component):
    template = """
      <c-CGroup class_="spinner-custom" gap="lg">
        <div class="spinner-custom__violet"><c-CSpinner label="Violet observatory task" /></div>
        <div class="spinner-custom__solar"><c-CSpinner label="Solar observatory task" /></div>
        <div class="spinner-custom__ice"><c-CSpinner label="Ice observatory task" /></div>
      </c-CGroup>
    """
    css = """
      :where(.spinner-custom > div) {
        display: grid;
        place-items: center;
        min-inline-size: 5rem;
        min-block-size: 5rem;
        border-radius: 0.75rem;
        background: light-dark(#f5f4ff, #17172a);
      }

      :where(.spinner-custom__violet) {
        --cui-spinner-color: #7c3aed;
        --cui-spinner-track-color: #ddd6fe;
        --cui-spinner-size: 2rem;
      }

      :where(.spinner-custom__solar) {
        --cui-spinner-color: #c2410c;
        --cui-spinner-track-color: #fed7aa;
        --cui-spinner-thickness: 0.24rem;
      }

      :where(.spinner-custom__ice) {
        --cui-spinner-color: #0891b2;
        --cui-spinner-track-color: #a5f3fc;
        --cui-spinner-duration: 1.2s;
      }
    """


preview = SpinnerCustomization()

preview  # noqa: B018

Choose the right indicator

Use CProgress when completion has a meaningful linear track or known value. Use CButton(loading=True) for a Button-owned pending action. Spinner does not own overlays, live announcements, task timing, or determinate values.

API reference

Inputs

CSpinner server inputs

Server inputs are passed in a template through <c-CSpinner ... /> or in Python through CSpinner(...).

InputTypeDefaultEffect
labelstrrequiredSets the required nonempty accessible task name.
intent"neutral" | "primary" | "success" | "warn" | "danger" (CSpinnerIntent)"primary"Selects the visual ring palette; surrounding text still carries meaning.
size"sm" | "md" | "lg" (CSpinnerSize)"md"Sets ring diameter and default thickness.
class_str | Mapping[str, bool] | Sequence[CClassValue] | None (CClassValue)NoneAdds root classes and merges them with attrs.
stylestr | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None (CStyleValue)NoneAdds root inline styles and merges them with attrs.
attrsMapping[str, object] | NoneNoneAdds copied trusted nonconflicting metadata, description relationships, visibility, and targeted Alpine attributes to the Spinner root.

CSpinner client inputs

Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CSpinner />.

InputTypeOmitted behaviorEffect
labelstringUses the server fallback.Controls the nonempty accessible task name; omission returns to server fallback.
intent"neutral" | "primary" | "success" | "warn" | "danger"Uses the server fallback.Controls the public visual palette reflection.
size"sm" | "md" | "lg"Uses the server fallback.Controls the public size reflection.

Slots

-

Events

-

Methods

-

CSS

CSS variables to theme the components. Set them on an ancestor or the component itself.

CSpinner CSS variables

Apply these variables to CSpinner or one of its ancestors.

VariableTypePurposeDefault
--cui-spinner-colorcolorActive ring arc.Intent-derived color.
--cui-spinner-track-colorcolorQuiet remainder of the ring.Current color mixed with transparency.
--cui-spinner-sizelengthRing diameter.Size-derived length.
--cui-spinner-thicknesslengthRing border width.Size-derived length.
--cui-spinner-durationtimeOne rotation duration when motion is allowed.0.75s.

Attributes

HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.

CSpinner attributes

AttributeElementTypeMeaning
roleRoot"progressbar"Exposes an indeterminate progress indicator.
aria-labelRootnonempty stringCarries the required accessible task name.
data-intentRoot"neutral" | "primary" | "success" | "warn" | "danger"Reflects the effective visual palette.
data-sizeRoot"sm" | "md" | "lg"Reflects effective diameter and default thickness.

Selectors

Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.

CSpinner selectors

SelectorElementPurpose
[data-citry-ui-part="spinner"]Spinner rootStable public root and attrs destination.

Interfaces

Aliases and data shapes referenced above.

Input type aliases

InterfaceDefinition
CClassValuestr | Mapping[str, bool] | Sequence[CClassValue]
CStyleValuestr | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue]
CSpinnerIntentLiteral["neutral", "primary", "success", "warn", "danger"]
CSpinnerSizeLiteral["sm", "md", "lg"]

Translation keys

-