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.

Install

#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 spinner
  • LoaderCircle - Circular loading spinner

#Other Compatible Icons

  • Any icon from the IconName enum 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

SizeMethodApproximate 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.",
);