gpuicn Components

Component 49 of 60

Spinner

An animated indicator that shows work in progress.

site/src/previews/spinner.rs

Starting the preview. GPUI compiles to WebAssembly, so the first load takes a few seconds. This browser has neither WebGPU nor WebGL 2, so the live preview cannot run. The code on this page still works. The preview did not start.

Spinner, running in the browser. The second tab shows the preview's own source, which includes its layout and demo state. For the smallest working example, see Usage below. Download the whole showcase for its imports and helpers.

Installation

After the one-time setup, run the command from your app root. The CLI writes the modules and keeps src/ui/mod.rs in step.

gpuicn add spinner

If you prefer to copy the files by hand, declare them yourself:

// src/ui/mod.rs
pub mod theme;
pub mod spinner;

// src/main.rs
mod ui;

Usage

Put this in a view module and return example() from its render method.

use crate::ui::spinner::Spinner;
use gpui_kit::IntoElement;

fn example() -> impl IntoElement {
    Spinner::new("history.loading").label("Loading older messages")
}

API reference

pub fn new(id: impl Into<ElementId>) -> Self

Creates a `Spinner` with a stable caller-owned ID.

pub fn label(mut self, label: impl Into<SharedString>) -> Self

Sets the accessible status text. Defaults to "Loading".

pub fn color(mut self, color: impl Into<Rgba>) -> Self

Sets the icon color. Defaults to the muted foreground.

Installed source, 80 lines
//! Nova-styled loading spinner.

use gpui_icons::{LucideIcon, lucide};
use gpui_kit::{
    Animation, AnimationExt as _, App, ElementId, InteractiveElement as _, IntoElement,
    ParentElement as _, RenderOnce, Rgba, Role, SharedString, StatefulInteractiveElement as _,
    Styled, Transformation, Window, div, percentage,
};

use super::theme::UiTheme;

/// One full turn of the spinner, matching Tailwind's `animate-spin`.
const TURN: std::time::Duration = std::time::Duration::from_secs(1);

/// A rotating loader icon announced as a status.
#[derive(IntoElement)]
pub struct Spinner {
    style: gpui_kit::StyleRefinement,
    id: ElementId,
    label: SharedString,
    color: Option<Rgba>,
}

impl Spinner {
    /// Creates a `Spinner` with a stable caller-owned ID.
    pub fn new(id: impl Into<ElementId>) -> Self {
        Self {
            style: gpui_kit::StyleRefinement::default(),
            id: id.into(),
            label: "Loading".into(),
            color: None,
        }
    }

    /// Sets the accessible status text. Defaults to "Loading".
    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
        self.label = label.into();
        self
    }

    /// Sets the icon color. Defaults to the muted foreground.
    pub fn color(mut self, color: impl Into<Rgba>) -> Self {
        self.color = Some(color.into());
        self
    }
}

impl RenderOnce for Spinner {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = UiTheme::read(cx);
        let icon = lucide(LucideIcon::LoaderCircle)
            .size_full()
            .text_color(self.color.unwrap_or(theme.colors.muted_foreground));
        let icon = if theme.motion.reduced || cx.reduce_motion() {
            icon.into_any_element()
        } else {
            icon.with_animation(
                (self.id.clone(), "spin"),
                Animation::new(TURN).repeat(),
                |icon, delta| icon.with_transformation(Transformation::rotate(percentage(delta))),
            )
            .into_any_element()
        };
        let root = div()
            .id(self.id)
            .role(Role::Status)
            .aria_label(self.label)
            .size(theme.space(4.))
            .flex_shrink_0()
            .child(icon);
        super::theme::apply_style(root, &self.style)
    }
}

impl Styled for Spinner {
    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {
        &mut self.style
    }
}

Platform notes

Visual reference: shadcn/ui 4.19.0 spinner, Neutral Nova. Implementation: crates/ui/registry/spinner/spinner.rs, using GPUI Kit 0.8.1.

The spinner is Lucide loader-circle at 16px, rotating once a second like Tailwind's animate-spin. It is announced with the status role and the label "Loading" unless Spinner::label replaces it. Size it with size; GPUI SVGs do not inherit the text color, so the icon uses the muted foreground unless Spinner::color sets one, for example the primary foreground inside a Button.

When UiTheme.motion.reduced or the platform's reduced-motion setting is on, the icon does not rotate and no animation frames are requested.