Component 46 of 60
Skeleton
A placeholder that stands in for content while it loads.
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 skeleton_preview(&self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let message = |id: &'static str, lines: &'static [f32]| {
div()
.w_full()
.flex()
.gap(px(12.))
.child(Skeleton::new((id, 100usize)).size(px(32.)).rounded_full())
.child(
div()
.flex_1()
.flex()
.flex_col()
.gap(px(8.))
.child(Skeleton::new((id, 101usize)).w(px(96.)).h(px(12.)))
.children(lines.iter().enumerate().map(move |(index, width)| {
Skeleton::new((id, index)).w(gpui_kit::relative(*width))
})),
)
};
div()
.w_full()
.max_w(px(360.0))
.flex()
.flex_col()
.gap(px(20.0))
.child(message("preview.skeleton.first", &[1., 0.6]))
.child(message("preview.skeleton.second", &[0.8]))
}
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.