Component 49 of 60
Spinner
An animated indicator that shows work in progress.
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.
fn spinner_preview(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = UiTheme::read(cx).clone();
div()
.flex()
.flex_col()
.items_center()
.gap(px(16.0))
.child(
div()
.flex()
.items_center()
.gap(px(16.0))
.child(Spinner::new("preview.spinner"))
.child(Spinner::new("preview.spinner.large").size(px(24.)))
.child(
Spinner::new("preview.spinner.primary")
.size(px(32.))
.color(theme.colors.primary),
),
)
.child(
Button::new("preview.spinner.button")
.disabled(true)
.child(
Spinner::new("preview.spinner.inline")
.label("Sending")
.color(theme.colors.primary_foreground),
)
.label("Sending…"),
)
.child(
div()
.flex()
.items_center()
.gap(px(8.0))
.text_size(px(13.))
.text_color(theme.colors.muted_foreground)
.child(Spinner::new("preview.spinner.history").label("Loading older messages"))
.child("Loading older messages"),
)
}
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.