gpuicn Components

Component 46 of 60

Skeleton

A placeholder that stands in for content while it loads.

site/src/previews/skeleton.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.

Skeleton, 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 skeleton

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

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

// src/main.rs
mod ui;

Usage

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

use crate::ui::skeleton::Skeleton;
use gpui_kit::{IntoElement, ParentElement as _, Styled as _, div, px};

fn example() -> impl IntoElement {
    div()
        .flex()
        .gap(px(12.))
        .child(Skeleton::new("row.avatar").size(px(32.)).rounded_full())
        .child(Skeleton::new("row.line").w(px(240.)))
}

API reference

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

Creates a `Skeleton` with a stable caller-owned ID for its pulse animation.

Installed source, 61 lines
//! Nova-styled loading placeholders.

use gpui_kit::{
    Animation, AnimationExt as _, App, ElementId, IntoElement, RenderOnce, Styled, Window, div,
    ease_in_out,
};

use super::theme::UiTheme;

/// One pulse cycle, matching Tailwind's `animate-pulse`.
const PULSE: std::time::Duration = std::time::Duration::from_secs(2);

/// A muted block that stands in for content while it loads.
///
/// Size it with `w`, `h` and `rounded` like any element. Skeletons are hidden from
/// assistive technology; announce loading with a nearby `Spinner` or label instead.
#[derive(IntoElement)]
pub struct Skeleton {
    style: gpui_kit::StyleRefinement,
    id: ElementId,
}

impl Skeleton {
    /// Creates a `Skeleton` with a stable caller-owned ID for its pulse animation.
    pub fn new(id: impl Into<ElementId>) -> Self {
        Self {
            style: gpui_kit::StyleRefinement::default(),
            id: id.into(),
        }
    }
}

impl RenderOnce for Skeleton {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = UiTheme::read(cx);
        let root = div()
            .w_full()
            .h(theme.space(4.))
            .flex_shrink_0()
            .rounded(theme.radius.md)
            .bg(theme.colors.muted);
        let root = super::theme::apply_style(root, &self.style);
        if theme.motion.reduced || cx.reduce_motion() {
            return root.into_any_element();
        }
        root.with_animation(
            self.id,
            Animation::new(PULSE).repeat().with_easing(ease_in_out),
            // Tailwind's pulse dips to half opacity halfway through each cycle.
            |root, delta| root.opacity(1. - 0.5 * (1. - (2. * delta - 1.).abs())),
        )
        .into_any_element()
    }
}

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

Platform notes

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

The skeleton is a muted block with the medium radius that pulses between full and half opacity every two seconds, like Tailwind's animate-pulse. Its default size is full width and 16px high; set w, h, size and rounded_full to match the content it replaces.

It takes a stable ID because GPUI keys the pulse animation on it. Under reduced motion the block is static. Skeletons carry no accessible role, so announce the loading state with a Spinner or label nearby.