Spinner
Spinner element displays an animated loading. Perfect for showing loading states, progress spinners, and other visual feedback during asynchronous operations. Features customizable icons, colors, sizes, and rotation animations.
#Import
use gpui_kit::component::spinner::Spinner;// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Spinner } from "spinner.slint";use iced::font::Weight;
use iced::widget::{column, container, mouse_area, row, scrollable, stack, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};#Usage
#Colors
// Default loader icon
Spinner::new()GridLayout {
spacing: 16px;
Spinner {
horizontal-stretch: 0;
}
Spinner {
horizontal-stretch: 0;
color: Theme.muted-foreground;
}
Spinner {
horizontal-stretch: 0;
color: Theme.info;
}
Spinner {
horizontal-stretch: 0;
color: Theme.success;
}
Spinner {
horizontal-stretch: 0;
color: Theme.warning;
}
Spinner {
horizontal-stretch: 0;
color: Theme.danger;
}
}// The Slint example's six, in its order: the foreground default, then the
// muted, info, success, warning and danger tokens.
let colors = row![
Spinner::<Message>::new(),
Spinner::new().color(tokens.muted_foreground),
Spinner::new().color(tokens.info),
Spinner::new().color(tokens.success),
Spinner::new().color(tokens.warning),
Spinner::new().color(tokens.danger),
]
.spacing(24.0)
.align_y(Alignment::Center);#Sizes
// Extra small spinner
Spinner::new().xsmall()
// Small spinner
Spinner::new().small()
// Medium spinner (default)
Spinner::new()
// Large spinner
Spinner::new().large()
// Custom size
Spinner::new().with_size(px(64.))GridLayout {
spacing: 16px;
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Spinner {
size: ControlSize.xsmall;
}
}
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Spinner {
size: ControlSize.small;
}
}
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Spinner {}
}
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Spinner {
size: ControlSize.large;
}
}
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Spinner {
width: 32px;
color: Theme.primary;
}
}
}// The four steps the catalog shares — 12, 14, 16 and 24 — and the custom
// 32px spinner the Slint example sets in the primary colour.
let sizes = row![
Spinner::new().size(ControlSize::XSmall),
Spinner::new().size(ControlSize::Small),
Spinner::new(),
Spinner::new().size(ControlSize::Large),
Spinner::new().diameter(32.0).color(tokens.primary),
]
.spacing(24.0)
.align_y(Alignment::Center);#In context
use gpui_kit::component::ActiveTheme;
// Blue spinner
Spinner::new()
.color(cx.theme().blue)
// Green spinner for success states
Spinner::new()
.color(cx.theme().green)
// Custom color
Spinner::new()
.color(cx.theme().cyan)GridLayout {
spacing: 8px;
Button {
horizontal-stretch: 0;
text: root.refreshing ? "Refreshing" : "Refresh";
icon: "refresh-cw";
loading: root.refreshing;
clicked => {
root.refreshing = true;
}
}
if root.refreshing: GridLayout {
horizontal-stretch: 0;
spacing: 8px;
VerticalLayout {
horizontal-stretch: 0;
alignment: center;
Spinner {
size: ControlSize.small;
color: Theme.muted-foreground;
}
}
Text {
horizontal-stretch: 0;
text: "Loading data…";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
vertical-alignment: center;
}
}
}
Rectangle {
height: 140px;
border-width: 1px;
border-color: Theme.border;
border-radius: Theme.radius-md;
clip: true;
VerticalLayout {
padding: 16px;
spacing: 8px;
Text {
text: "Quarterly report";
color: Theme.foreground;
font-size: Theme.font-size-sm;
font-weight: 600;
}
Text {
text: "Revenue, active users and retention for the last three months.";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
}
}
Rectangle {
background: Theme.background.with-alpha(0.8);
VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
Spinner {
size: ControlSize.large;
color: Theme.primary;
}
}
Text {
text: "Loading…";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
horizontal-alignment: center;
}
}
}
}// The Refresh button carries the spinner in its icon slot, the loading row
// is the reference's "Loading data…", and the card is its report card with
// a large primary spinner over a translucent veil.
let context = column![
row![
Button::new("Refresh")
.variant(Variant::Primary)
.icon(
Spinner::new()
.size(ControlSize::Small)
.color(tokens.primary_foreground)
)
.on_press(Message::Pressed("Refresh")),
row![
Spinner::new()
.size(ControlSize::Small)
.color(tokens.muted_foreground),
text("Loading data…").size(theme::FONT_SIZE_SM).style(muted),
]
.spacing(8.0)
.align_y(Alignment::Center),
]
.spacing(16.0)
.align_y(Alignment::Center),
loading_panel(
tokens,
"Quarterly report",
"Revenue, active users and retention for the last three months."
),
]
.spacing(16.0);#Spinner with Custom Icon
use gpui_kit::component::IconName;
// Loading circle icon
Spinner::new()
.icon(IconName::LoaderCircle)
// Large loading circle with custom color
Spinner::new()
.icon(IconName::LoaderCircle)
.large()
.color(cx.theme().cyan)
// Different loading icons
Spinner::new()
.icon(IconName::Loader)
.color(cx.theme().primary)HorizontalLayout {
spacing: 16px;
alignment: center;
Spinner {
icon: "loader-circle";
size: ControlSize.large;
color: Theme.primary;
}
Spinner {
icon: "refresh-cw";
size: ControlSize.large;
color: Theme.success;
}
Spinner {
icon: "refresh-cw";
size: ControlSize.large;
color: Theme.warning;
}
}// iced's spinner draws the `loader-circle` ring itself and takes no glyph
// name, so a custom mark is the `Icon` component's own turn — the same
// clock and the same 800ms — with the glyphs the Slint example spins.
let custom_icons = row![
Icon::<Message>::new(svg::Handle::from_memory(LOADER))
.size(ControlSize::Large)
.color(tokens.primary)
.spinning(true),
Icon::new(svg::Handle::from_memory(REFRESH))
.size(ControlSize::Large)
.color(tokens.success)
.spinning(true),
Icon::new(svg::Handle::from_memory(REFRESH))
.size(ControlSize::Large)
.color(tokens.warning)
.spinning(true),
]
.spacing(16.0)
.align_y(Alignment::Center);#Loading States
// Simple loading spinner
Spinner::new()
// Loading with custom color
Spinner::new()
.color(cx.theme().blue)
// Large loading spinner
Spinner::new()
.large()
.color(cx.theme().primary)SlintNo Slint version of this example yet.
// The page's three: a plain spinner, one in a chosen colour, and a large one
// in the primary colour.
let states = row![
Spinner::new(),
Spinner::new().color(tokens.info),
Spinner::new()
.size(ControlSize::Large)
.color(tokens.primary),
]
.spacing(24.0)
.align_y(Alignment::Center);#Different Loading Icons
// Default loader (line spinner)
Spinner::new()
.color(cx.theme().muted_foreground)
// Circle loader
Spinner::new()
.icon(IconName::LoaderCircle)
.color(cx.theme().blue)
// Large circle loader with custom color
Spinner::new()
.icon(IconName::LoaderCircle)
.large()
.color(cx.theme().green)SlintNo Slint version of this example yet.
// The loader ring, then the `refresh-cw` mark in two colours — the glyphs
// the page names, all turning on the same clock.
let different = row![
Spinner::new().color(tokens.muted_foreground),
Icon::new(svg::Handle::from_memory(LOADER))
.size(ControlSize::Large)
.color(tokens.info)
.spinning(true),
Icon::new(svg::Handle::from_memory(REFRESH))
.size(ControlSize::Large)
.color(tokens.success)
.spinning(true),
]
.spacing(24.0)
.align_y(Alignment::Center);#Status Spinners
// Loading state
Spinner::new()
.small()
.color(cx.theme().muted_foreground)
// Processing state
Spinner::new()
.icon(IconName::LoaderCircle)
.color(cx.theme().blue)
// Success processing (still animating)
Spinner::new()
.icon(IconName::LoaderCircle)
.color(cx.theme().green)SlintNo Slint version of this example yet.
// The three states the page draws: resting, processing and succeeded, each
// still turning.
let status = row![
Spinner::new()
.size(ControlSize::Small)
.color(tokens.muted_foreground),
Spinner::new().size(ControlSize::Small).color(tokens.info),
Spinner::new()
.size(ControlSize::Small)
.color(tokens.success),
]
.spacing(24.0)
.align_y(Alignment::Center);#Size Variations
// Extra small for inline text
Spinner::new()
.xsmall()
.color(cx.theme().muted_foreground)
// Small for buttons
Spinner::new()
.small()
.color(cx.theme().primary_foreground)
// Medium for general use (default)
Spinner::new()
.color(cx.theme().primary)
// Large for prominent loading states
Spinner::new()
.large()
.color(cx.theme().blue)
// Custom size for specific requirements
Spinner::new()
.with_size(px(32.))
.color(cx.theme().orange)SlintNo Slint version of this example yet.
// The same ladder with its numbers under it, which is how the page's table
// reads: 12 for inline text, 14 in buttons, 16 by default, 24 for a
// prominent state, 32 when the caller wants its own.
let variations = row![
sized(
"12 px",
Spinner::new()
.size(ControlSize::XSmall)
.color(tokens.muted_foreground)
),
sized(
"14 px",
Spinner::new()
.size(ControlSize::Small)
.color(tokens.primary)
),
sized("16 px", Spinner::new().color(tokens.primary)),
sized(
"24 px",
Spinner::new().size(ControlSize::Large).color(tokens.info)
),
sized("32 px", Spinner::new().diameter(32.0).color(tokens.success)),
]
.spacing(16.0);#In UI Components
// In a button
Button::new("submit-btn")
.loading(true)
.icon(
Spinner::new()
.small()
.color(cx.theme().primary_foreground)
)
.label("Loading...")
// In a card header
div()
.flex()
.items_center()
.gap_2()
.child("Processing...")
.child(
Spinner::new()
.small()
.color(cx.theme().muted_foreground)
)
// Full-screen loading
div()
.flex()
.items_center()
.justify_center()
.h_full()
.w_full()
.child(
Spinner::new()
.large()
.color(cx.theme().primary)
)SlintNo Slint version of this example yet.
// The page's three uses: inside a button, beside a header's text, and
// centred in a page of its own.
let in_components = column![
Button::new("Submit")
.variant(Variant::Primary)
.icon(
Spinner::new()
.size(ControlSize::Small)
.color(tokens.primary_foreground)
)
.on_press(Message::Pressed("Submit")),
row![
text("Processing…").size(theme::FONT_SIZE_SM),
Spinner::new()
.size(ControlSize::Small)
.color(tokens.muted_foreground),
]
.spacing(8.0)
.align_y(Alignment::Center),
container(
Spinner::new()
.size(ControlSize::Large)
.color(tokens.primary)
)
.width(Length::Fill)
.height(Length::Fixed(88.0))
.center_x(Length::Fill)
.center_y(Length::Fixed(88.0)),
]
.spacing(16.0);#Available Icons
The Spinner component supports various loading and progress icons:
#Loading Icons
Loader(default) - Rotating line spinnerLoaderCircle- Circular loading spinner
#Other Compatible Icons
- Any icon from the
IconNameenum can be used, though loading-specific icons work best with the rotation animation
#Animation
The Spinner component features a built-in rotation animation:
- Duration: 0.8 seconds (configurable via speed parameter)
- Easing: Ease-in-out transition
- Repeat: Infinite loop
- Transform: 360-degree rotation
#Size Reference
| Size | Method | Approximate Pixels |
|---|---|---|
| Extra Small | .xsmall() | ~12px |
| Small | .small() | ~14px |
| Medium | (default) | ~16px |
| Large | .large() | ~24px |
| Custom | .with_size(px(n)) | n px |
#Performance Considerations
- The animation uses CSS transforms for optimal performance
- Multiple spinners on the same page share the same animation timing
- The component is lightweight and suitable for frequent updates
- Consider using smaller sizes for better performance with many spinners
#Common Patterns
#Conditional Loading
// Show spinner only when loading
.when(is_loading, |this| {
this.child(
Spinner::new()
.small()
.color(cx.theme().muted_foreground)
)
})SlintNo Slint version of this example yet.
// The page draws the spinner only while its flag is set; the button under
// the row is that flag, so the sample can be driven rather than described.
let conditional: Element<Message> = if app.loading {
row![
Spinner::new()
.size(ControlSize::Small)
.color(tokens.primary),
text("Loading data…").size(theme::FONT_SIZE_SM),
]
.spacing(8.0)
.align_y(Alignment::Center)
.into()
} else {
text("Nothing is loading.")
.size(theme::FONT_SIZE_SM)
.style(muted)
.into()
};
let conditional_sample = column![
container(conditional)
.width(Length::Fill)
.center_y(Length::Fixed(24.0)),
Button::new(if app.loading {
"Finish loading"
} else {
"Start loading"
})
.on_press(Message::ToggleLoading),
]
.spacing(12.0);#Loading with Text
// Loading text with spinner
h_flex()
.items_center()
.gap_2()
.child(
Spinner::new()
.small()
.color(cx.theme().primary)
)
.child("Loading data...")SlintNo Slint version of this example yet.
let with_text = row![
Spinner::new()
.size(ControlSize::Small)
.color(tokens.primary),
text("Loading data…").size(theme::FONT_SIZE_SM).style(muted),
]
.spacing(8.0)
.align_y(Alignment::Center);#Overlay Loading
// Full overlay with spinner
div()
.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.bg(cx.theme().background.alpha(0.8))
.child(
v_flex()
.items_center()
.gap_3()
.child(
Spinner::new()
.large()
.color(cx.theme().primary)
)
.child("Loading...")
)SlintNo Slint version of this example yet.
// The page's full-area overlay, sized to the gallery: the content dimmed by
// a translucent card and a large primary spinner over it.
let overlay = loading_panel(
tokens,
"Weekly digest",
"Twenty-four new items since your last visit.",
);