Skeleton
Use to show a placeholder while content is loading.
Basic
Simple text-line placeholders.
Card
Skeleton layout mimicking a card with avatar and text.
Form
Skeleton layout for a loading form.
Table
Skeleton rows for a loading table.
FreeSkeleton
Loading placeholders.
Install with the Proa CLI
$ proa ui add skeletonInstalls the reviewed component source, dependencies, shared support files, and required legal notices.
CLI and registry setup →recipe.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Skeleton recipe - styling for loading placeholder
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
pub const SKELETON_BASE: &str = tw_join!("animate-pulse", "rounded-md", "bg-muted");
/// Optional author inputs for the Skeleton recipe.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SkeletonRecipeProps;
/// Static named root for Skeleton.
#[derive(Debug, Clone, Copy)]
pub struct SkeletonRecipe {
pub root: &'static str,
}
impl SkeletonRecipe {
Skeleton source
Browse and copy the reviewed source included with this Free component.
md.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
use super::Skeleton;
use proa_core::WebContext;
use proa_core::{MdRenderSync, WriteBuf, WriteError};
/// Skeleton is visual-only — renders nothing in markdown.
impl<Loader: ::proa_core::DataLoader, B: WriteBuf> MdRenderSync<Loader, B> for Skeleton {
fn render_md(self, _cx: &mut WebContext<Loader, B>) -> Result<(), WriteError> {
Ok(())
}
}
mod.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Skeleton component - animated loading placeholder
mod md;
pub mod recipe;
mod skeleton;
pub use recipe::{
skeleton_classes, skeleton_recipe, SkeletonRecipe, SkeletonRecipeProps, SkeletonStyle,
SKELETON_RECIPE,
};
pub use skeleton::Skeleton;
recipe.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Skeleton recipe - styling for loading placeholder
use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};
pub const SKELETON_BASE: &str = tw_join!("animate-pulse", "rounded-md", "bg-muted");
/// Optional author inputs for the Skeleton recipe.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SkeletonRecipeProps;
/// Static named root for Skeleton.
#[derive(Debug, Clone, Copy)]
pub struct SkeletonRecipe {
pub root: &'static str,
}
impl SkeletonRecipe {
pub const fn resolve(&'static self, _props: SkeletonRecipeProps) -> SkeletonStyle {
SkeletonStyle { recipe: self }
}
}
/// Resolved, allocation-free Skeleton root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct SkeletonStyle {
recipe: &'static SkeletonRecipe,
}
impl AttrValue for SkeletonStyle {
#[inline(always)]
fn should_render(&self) -> bool {
!self.recipe.root.is_empty()
}
#[inline(always)]
fn render_attr_value<B: WriteBuf, L: DataLoader>(
&self,
cx: &mut WebContext<L, B>,
) -> Result<(), WriteError> {
cx.out.extend_static(self.recipe.root.as_bytes())
}
}
pub const SKELETON_RECIPE: SkeletonRecipe = SkeletonRecipe {
root: SKELETON_BASE,
};
pub const fn skeleton_recipe() -> &'static SkeletonRecipe {
&SKELETON_RECIPE
}
/// Compatibility accessor for the former standalone recipe function.
pub const fn skeleton_classes() -> &'static str {
SKELETON_RECIPE.root
}
impl crate::variant_spec::ComponentSpec for SkeletonRecipe {
const NAME: &'static str = "Skeleton";
fn base_classes(&self) -> &'static str {
self.root
}
fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn has_pulse_animation() {
let classes = skeleton_classes();
assert!(classes.contains("animate-pulse"));
assert!(classes.contains("rounded-md"));
assert!(classes.contains("bg-muted"));
assert_eq!(skeleton_recipe().root, classes);
}
#[test]
fn recipe_is_const_and_copy() {
const STYLE: SkeletonStyle = SKELETON_RECIPE.resolve(SkeletonRecipeProps);
let _copy = STYLE;
assert!(STYLE.should_render());
}
}
skeleton.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Skeleton component implementation
use super::recipe::{SkeletonRecipeProps, SKELETON_RECIPE};
use proa_core::{WebContext, WebRenderSync, WriteBuf};
use proa_macros::html_sync;
/// Skeleton - an animated loading placeholder
///
/// Renders a `<div>` with a pulse animation and muted background.
/// Size and shape are controlled via the `class` prop.
///
/// # Example
/// ```ignore
/// Skeleton { class: Some("h-4 w-[250px]"), id: None }
/// Skeleton { class: Some("h-12 w-12 rounded-full"), id: None }
/// ```
#[derive(Default)]
pub struct Skeleton {
pub class: Option<&'static str>,
pub id: Option<&'static str>,
}
impl<B: WriteBuf, L: ::proa_core::DataLoader> WebRenderSync<L, B> for Skeleton {
fn render(
self,
cx: &mut WebContext<L, B>,
) -> Result<(), proa_core::shared::write_buf::WriteError> {
let style = SKELETON_RECIPE.resolve(SkeletonRecipeProps);
html_sync! {
<div
data-skeleton
data-slot="skeleton"
class={proa_macros::text!("{} {}", style, self.class)}
id={self.id}
></div>
}
.render(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proa_core::ctx::Ctx;
#[test]
fn renders_div_with_pulse() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Skeleton {
class: None,
id: None,
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("<div"));
assert!(html.contains("animate-pulse"));
assert!(html.contains("bg-muted"));
assert!(html.contains("rounded-md"));
assert!(html.contains("data-slot=\"skeleton\""));
}
#[test]
fn accepts_custom_class() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Skeleton {
class: Some("h-4 w-[250px]"),
id: None,
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("h-4"));
assert!(html.contains("w-[250px]"));
assert!(html.contains("animate-pulse"));
}
#[test]
fn rounded_full_override() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Skeleton {
class: Some("h-12 w-12 rounded-full"),
id: None,
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("rounded-full"));
}
#[test]
fn accepts_id() {
let mut out = Vec::new();
let mut cx = Ctx::with_buffer(&mut out);
Skeleton {
class: None,
id: Some("loading-name"),
}
.render(&mut cx)
.unwrap();
let html = String::from_utf8(out).unwrap();
assert!(html.contains("id=\"loading-name\""));
}
}