Skeleton
Skeleton is a placeholder to show a loading state and the expected shape of a component.
Usage
import { Skeleton } from '@lenso/ui';"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ panel: { width: 250, display: "flex", flexDirection: "column", gap: 20, borderRadius: "var(--radius-lg)", backgroundColor: "transparent", padding: 16, boxShadow: "var(--shadow-panel)", }, text: { display: "flex", flexDirection: "column", gap: 12 }, picture: { height: 128, borderRadius: "var(--radius-lg)" }, short: { height: 12, width: "60%", borderRadius: "var(--radius-lg)" }, long: { height: 12, width: "80%", borderRadius: "var(--radius-lg)" }, shortest: { height: 12, width: "40%", borderRadius: "var(--radius-lg)" },});export function Basic() { return ( <div {...stylex.props(styles.panel)}> <Skeleton xstyle={styles.picture} /> <div {...stylex.props(styles.text)}> <Skeleton xstyle={styles.short} /> <Skeleton xstyle={styles.long} /> <Skeleton xstyle={styles.shortest} /> </div> </div> );}export default Basic;Local adaptation source above. Derived from HeroUI v3.2.6 source.
Examples
Text Content
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ text: { width: "100%", maxWidth: 448, display: "flex", flexDirection: "column", gap: 12 }, line: { height: 16, borderRadius: "var(--radius)" }, full: { width: "100%" }, five: { width: "83.333333%" }, four: { width: "66.666667%" }, half: { width: "50%" },});export function TextContent() { return ( <div {...stylex.props(styles.text)}> <Skeleton xstyle={[styles.line, styles.full]} /> <Skeleton xstyle={[styles.line, styles.five]} /> <Skeleton xstyle={[styles.line, styles.four]} /> <Skeleton xstyle={[styles.line, styles.full]} /> <Skeleton xstyle={[styles.line, styles.half]} /> </div> );}export default TextContent;Local adaptation source above. Derived from HeroUI v3.2.6 source.
User Profile
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ row: { display: "flex", alignItems: "center", gap: 12 }, avatar: { height: 40, width: 40, flexShrink: 0, borderRadius: "50%" }, text: { flex: 1, display: "flex", flexDirection: "column", gap: 8 }, name: { height: 12, width: 144, borderRadius: "var(--radius-lg)" }, detail: { height: 12, width: 96, borderRadius: "var(--radius-lg)" },});export function UserProfile() { return ( <div {...stylex.props(styles.row)}> <Skeleton xstyle={styles.avatar} /> <div {...stylex.props(styles.text)}> <Skeleton xstyle={styles.name} /> <Skeleton xstyle={styles.detail} /> </div> </div> );}export default UserProfile;Local adaptation source above. Derived from HeroUI v3.2.6 source.
List Items
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ list: { width: "100%", maxWidth: 384, display: "flex", flexDirection: "column", gap: 16 }, row: { display: "flex", alignItems: "center", gap: 12 }, avatar: { height: 40, width: 40, flexShrink: 0, borderRadius: "var(--radius-lg)" }, text: { flex: 1, display: "flex", flexDirection: "column", gap: 8 }, full: { height: 12, width: "100%", borderRadius: "var(--radius)" }, short: { height: 12, width: "80%", borderRadius: "var(--radius)" },});export function List() { return ( <div {...stylex.props(styles.list)}> {Array.from({ length: 3 }, (_, index) => ( <div key={index} {...stylex.props(styles.row)}> <Skeleton xstyle={styles.avatar} /> <div {...stylex.props(styles.text)}> <Skeleton xstyle={styles.full} /> <Skeleton xstyle={styles.short} /> </div> </div> ))} </div> );}export default List;Local adaptation source above. Derived from HeroUI v3.2.6 source.
Grid
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ grid: { display: "grid", width: "100%", maxWidth: 576, gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 16, }, bone: { height: 96, borderRadius: "var(--radius-xl)" },});export function Grid() { return ( <div {...stylex.props(styles.grid)}> <Skeleton xstyle={styles.bone} /> <Skeleton xstyle={styles.bone} /> <Skeleton xstyle={styles.bone} /> </div> );}export default Grid;Local adaptation source above. Derived from HeroUI v3.2.6 source.
Single Shimmer
A synchronized shimmer effect that passes over all skeleton elements at once. Apply the skeleton--shimmer class to a parent container and set animationType="none" on child skeletons.
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ grid: { display: "grid", width: "100%", maxWidth: 576, gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 16, borderRadius: "var(--radius-xl)", backgroundColor: "transparent", }, bone: { height: 96, borderRadius: "var(--radius-xl)" },});export function SingleShimmer() { return ( <Skeleton xstyle={styles.grid}> <Skeleton animationType="none" xstyle={styles.bone} /> <Skeleton animationType="none" xstyle={styles.bone} /> <Skeleton animationType="none" xstyle={styles.bone} /> </Skeleton> );}export default SingleShimmer;Local adaptation source above. Derived from HeroUI v3.2.6 source.
Animation Types
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ grid: { display: "grid", width: "100%", maxWidth: 576, gridTemplateColumns: { default: "1fr", "@media (min-width: 640px)": "repeat(2, minmax(0, 1fr))", "@media (min-width: 1024px)": "repeat(3, minmax(0, 1fr))", }, gap: 24, }, column: { display: "flex", flexDirection: "column", gap: 8 }, label: { fontSize: 12, lineHeight: "16px", color: "var(--muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }, panel: { display: "flex", flexDirection: "column", gap: 12, borderRadius: "var(--radius-lg)", backgroundColor: "transparent", padding: 16, boxShadow: "var(--shadow-panel)", }, picture: { height: 80, borderRadius: "var(--radius-lg)" }, short: { height: 12, width: "60%", borderRadius: "var(--radius-lg)" }, long: { height: 12, width: "80%", borderRadius: "var(--radius-lg)" },});export function AnimationTypes() { return ( <div {...stylex.props(styles.grid)}> {(["shimmer", "pulse", "none"] as const).map((animationType) => ( <div key={animationType} {...stylex.props(styles.column)}> <p {...stylex.props(styles.label)}> {animationType === "shimmer" ? "Shimmer" : animationType === "pulse" ? "Pulse" : "None"} </p> <div {...stylex.props(styles.panel)}> <Skeleton animationType={animationType} xstyle={styles.picture} /> <Skeleton animationType={animationType} xstyle={styles.short} /> <Skeleton animationType={animationType} xstyle={styles.long} /> </div> </div> ))} </div> );}export default AnimationTypes;Local adaptation source above. Derived from HeroUI v3.2.6 source.
Customization
Tailwind CSS
"use client";// Adapted from HeroUI v3.2.6 (Apache-2.0).import { Skeleton } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const shine = stylex.keyframes({ from: { translate: "-100% 0" }, to: { translate: "100% 0" },});const styles = stylex.create({ panel: { width: 250, display: "flex", flexDirection: "column", gap: 20, borderRadius: "var(--radius-xl)", border: "1px solid color-mix(in oklab, var(--border) 80%, transparent)", backgroundColor: "var(--surface)", padding: 16, boxShadow: "0 1px 2px 0 rgb(0 0 0 / .05), 0 0 0 1px light-dark(rgb(0 0 0 / .05), rgb(255 255 255 / .1))", }, text: { display: "flex", flexDirection: "column", gap: 12 }, bone: { borderRadius: "var(--radius-lg)", backgroundColor: "light-dark(oklch(92.2% 0 0 / .9), oklch(26.9% 0 0 / .9))", "::after": { position: "absolute", inset: 0, width: "100%", height: "100%", content: '""', translate: "-100% 0", backgroundImage: "linear-gradient(120deg, transparent 10%, light-dark(rgb(255 255 255 / .3), rgb(255 255 255 / .1)) 45%, light-dark(rgb(255 255 255 / .1), rgb(255 255 255 / .04)) 55%, transparent 90%)", animationName: { default: shine, "@media (prefers-reduced-motion: reduce)": "none" }, animationDuration: "3s", animationTimingFunction: "ease-in-out", animationIterationCount: "infinite", pointerEvents: "none", }, }, picture: { height: 128 }, short: { height: 12, width: "60%" }, long: { height: 12, width: "80%" }, shortest: { height: 12, width: "40%" },});export function CustomStyles() { return ( <div {...stylex.props(styles.panel)}> <Skeleton xstyle={[styles.bone, styles.picture]} /> <div {...stylex.props(styles.text)}> <Skeleton xstyle={[styles.bone, styles.short]} /> <Skeleton xstyle={[styles.bone, styles.long]} /> <Skeleton xstyle={[styles.bone, styles.shortest]} /> </div> </div> );}export default CustomStyles;Local adaptation source above. Derived from HeroUI v3.2.6 source.
Global CSS
To customize the Skeleton component classes, you can use the @layer components directive.
Learn more.
@layer components { /* Base skeleton styles */ .skeleton { @apply bg-surface-secondary/50; /* Change base background */ }
/* Shimmer animation gradient */ .skeleton--shimmer:before { @apply viasurface; /* Change shimmer gradient color */ }
/* Pulse animation */ .skeleton--pulse { @apply animate-pulse opacity-75; /* Customize pulse animation */ }
/* No animation variant */ .skeleton--none { @apply opacity-50; /* Style for static skeleton */ }}Global Animation Configuration
You can set a default animation type for all Skeleton components in your application by defining the --skeleton-animation CSS variable:
/* In your global CSS file */:root { /* Possible values: shimmer, pulse, none */ --skeleton-animation: pulse;}
/* You can also set different values for light/dark themes */.light, [data-theme="light"] { --skeleton-animation: shimmer;}
.dark, [data-theme="dark"] { --skeleton-animation: pulse;}This global setting will be overridden by the animationType prop when specified on individual components.
Styling Reference
HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The Skeleton component uses these CSS classes (View source styles):
Base Class [!toc]
.skeleton - Base skeleton styles with background and rounded corners
Animation Variant Classes [!toc]
.skeleton--shimmer- Adds shimmer animation with gradient effect (default).skeleton--pulse- Adds pulse animation using Tailwind's animate-pulse.skeleton--none- No animation, static skeleton
Animation
The Skeleton component supports three animation types, each with different visual effects:
Shimmer Animation [!toc]
The shimmer effect creates a gradient that moves across the skeleton element:
.skeleton--shimmer:before { @apply animate-skeleton via-surface-3 absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent to-transparent content-[''];}The shimmer animation is defined in the theme using:
@theme inline { --animate-skeleton: skeleton 2s linear infinite;
@keyframes skeleton { 100% { transform: translateX(200%); } }}Pulse Animation [!toc]
The pulse animation uses Tailwind's built-in animate-pulse utility:
.skeleton--pulse { @apply animate-pulse;}No Animation [!toc]
For static skeletons without any animation:
.skeleton--none { /* No animation styles applied */}API Reference
Skeleton
| Prop | Type | Default | Description |
|---|---|---|---|
animationType | "shimmer" | "pulse" | "none" | "shimmer" or CSS variable | The animation type for the skeleton. Can be globally configured via --skeleton-animation CSS variable |
className | string | - | Additional CSS classes |