UI/UI
Skeleton
Display placeholder content while loading to improve perceived performance.
Text Lines
import { Skeleton, SkeletonText, SkeletonAvatar } from "@/components/ui/Skeleton";
<div className="space-y-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-5/6" />
</div>
The Skeleton component is used to display placeholder content while loading data, providing users with visual feedback about the loading state and improving perceived performance.
Installation
Install following dependencies:
npm install class-variance-authority
pnpm add class-variance-authority
yarn add class-variance-authority
bun add class-variance-authority
Add required animation and keyframes to your CSS file or tailwind config file based on your Tailwind version.
@theme {
--animate-shimmer: shimmer 1.5s infinite linear;
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
module.exports = {
theme: {
extend: {
keyframes: {
shimmer: {
"0%": { transform: "translateX(-100%)" },
"100%": { transform: "translateX(100%)" },
},
}
animations: {
shimmer: "shimmer 1.5s infinite linear",
},
}
}
}
Copy and paste the following code into your project.
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const skeletonVariants = cva(
"animate-pulse rounded-[var(--radius)] bg-[hsl(var(--hu-accent))]",
{
variants: {
variant: {
default: "bg-[hsl(var(--hu-accent))]",
secondary: "bg-[hsl(var(--hu-accent))]/20",
text: "bg-[hsl(var(--hu-accent))] rounded-md",
circle: "rounded-full",
avatar: "rounded-full bg-[hsl(var(--hu-accent))]",
},
size: {
sm: "h-4",
default: "h-6",
lg: "h-8",
xl: "h-10",
"2xl": "h-12",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface SkeletonProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof skeletonVariants> {
/**
* Custom width for the skeleton
*/
width?: string | number;
/**
* Custom height for the skeleton
*/
height?: string | number;
/**
* Animation speed in seconds
*/
duration?: number;
/**
* Whether to show shimmer effect
*/
shimmer?: boolean;
}
const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
(
{
className,
variant,
size,
width,
height,
duration = 2,
shimmer = true,
style,
...props
},
ref,
) => {
const customStyle = {
width: typeof width === "number" ? `${width}px` : width,
height: typeof height === "number" ? `${height}px` : height,
animationDuration: `${duration}s`,
...style,
};
return (
<div
ref={ref}
className={cn(
skeletonVariants({ variant, size }),
shimmer && "relative overflow-hidden",
shimmer &&
"before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent",
className,
)}
style={customStyle}
{...props}
/>
);
},
);
Skeleton.displayName = "Skeleton";
// Pre-built skeleton components for common use cases
const SkeletonText = React.forwardRef<
HTMLDivElement,
Omit<SkeletonProps, "variant">
>(({ className, ...props }, ref) => (
<Skeleton
ref={ref}
variant="text"
className={cn("w-full", className)}
{...props}
/>
));
SkeletonText.displayName = "SkeletonText";
const SkeletonAvatar = React.forwardRef<
HTMLDivElement,
Omit<SkeletonProps, "variant">
>(({ className, size = "default", ...props }, ref) => {
const avatarSizeMap = {
sm: "w-8 h-8",
default: "w-10 h-10",
lg: "w-12 h-12",
xl: "w-16 h-16",
"2xl": "w-20 h-20",
};
const avatarSize =
avatarSizeMap[size as keyof typeof avatarSizeMap] || "w-10 h-10";
return (
<Skeleton
ref={ref}
variant="avatar"
className={cn(avatarSize, className)}
{...props}
/>
);
});
SkeletonAvatar.displayName = "SkeletonAvatar";
const SkeletonButton = React.forwardRef<
HTMLDivElement,
Omit<SkeletonProps, "variant">
>(({ className, size = "default", ...props }, ref) => {
const buttonHeight: Record<string, string> = {
sm: "h-8",
default: "h-10",
lg: "h-11",
xl: "h-12",
"2xl": "h-14",
};
const selectedHeight = buttonHeight[size as string] || "h-10";
return (
<Skeleton
ref={ref}
className={cn(selectedHeight, "w-20 rounded-[var(--radius)]", className)}
{...props}
/>
);
});
SkeletonButton.displayName = "SkeletonButton";
const SkeletonCard = React.forwardRef<
HTMLDivElement,
Omit<SkeletonProps, "variant"> & {
showImage?: boolean;
showHeader?: boolean;
showFooter?: boolean;
}
>(
(
{
className,
showImage = true,
showHeader = true,
showFooter = true,
...props
},
ref,
) => (
<div
ref={ref}
className={cn(
"rounded-[var(--radius)] border bg-card p-0 overflow-hidden",
className,
)}
{...props}
>
{showImage && (
<Skeleton className="w-full h-48 rounded-none rounded-t-xl" />
)}
<div className="p-6 space-y-4">
{showHeader && (
<div className="space-y-2">
<Skeleton className="h-6 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</div>
)}
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
<Skeleton className="h-4 w-3/5" />
</div>
{showFooter && (
<div className="flex justify-between items-center pt-2">
<Skeleton className="h-8 w-20" />
<Skeleton className="h-8 w-16" />
</div>
)}
</div>
</div>
),
);
SkeletonCard.displayName = "SkeletonCard";
export {
Skeleton,
SkeletonText,
SkeletonAvatar,
SkeletonButton,
SkeletonCard,
skeletonVariants,
};
npx hextaui@latest add skeleton
pnpm dlx hextaui@latest add skeleton
yarn dlx hextaui@latest add skeleton
bun x hextaui@latest add skeleton
Usage
import {
Skeleton,
SkeletonText,
SkeletonAvatar,
SkeletonButton,
SkeletonCard,
} from "@/components/ui/Skeleton";
<Skeleton className="h-4 w-full" />
<SkeletonText className="w-3/4" />
<SkeletonAvatar size="lg" />
<SkeletonButton size="default" />
<SkeletonCard />
Examples
Basic Skeleton
Text Lines
<div className="space-y-2">
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-4 w-5/6" />
</div>
Avatar Skeletons
Avatar Sizes
<div className="flex flex-wrap items-center gap-4">
<SkeletonAvatar size="sm" />
<SkeletonAvatar size="default" />
<SkeletonAvatar size="lg" />
<SkeletonAvatar size="xl" />
<SkeletonAvatar size="2xl" />
</div>
Button Skeletons
Button Sizes
<div className="flex flex-wrap items-center gap-4">
<SkeletonButton size="sm" />
<SkeletonButton size="default" />
<SkeletonButton size="lg" />
</div>
Card Skeleton
Card Layouts
<div className="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2">
{/* Full card with image, header, content, and footer */}
<SkeletonCard />
{/* Card without image */}
<SkeletonCard showImage={false} />
</div>
<div className="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 mt-4">
{/* Card without footer */}
<SkeletonCard showFooter={false} />
{/* Card without header */}
<SkeletonCard showHeader={false} />
{/* Card with only content */}
<SkeletonCard showImage={false} showFooter={false} />
</div>
Profile Card Skeleton
Profile Card
<div className="rounded-lg border p-4 sm:p-6 space-y-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-4 sm:space-y-0 sm:space-x-4">
<SkeletonAvatar size="lg" />
<div className="space-y-2 flex-1 w-full">
<Skeleton className="h-4 w-full sm:w-1/3" />
<Skeleton className="h-3 w-3/4 sm:w-1/2" />
</div>
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-4/5" />
<Skeleton className="h-4 w-3/5" />
</div>
<div className="flex flex-col sm:flex-row justify-between gap-4">
<SkeletonButton size="sm" />
<SkeletonButton size="sm" />
</div>
</div>
Article List Skeleton
Article List
<div className="space-y-4 sm:space-y-6">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4"
>
<Skeleton className="w-full sm:w-16 h-32 sm:h-16 rounded-lg" />
<div className="flex-1 space-y-2">
<SkeletonText className="w-full sm:w-3/4" />
<SkeletonText className="w-4/5 sm:w-1/2" size="sm" />
<div className="flex flex-wrap items-center gap-2">
<SkeletonAvatar size="sm" />
<SkeletonText className="w-20" size="sm" />
<SkeletonText className="w-16" size="sm" />
</div>
</div>
</div>
))}
</div>
Data Table Skeleton
Data Table
<div className="rounded-md border overflow-x-auto">
<div className="min-w-[600px]">
{/* Header */}
<div className="border-b p-4">
<div className="grid grid-cols-4 gap-4 items-center">
<SkeletonText size="sm" />
<SkeletonText size="sm" />
<SkeletonText size="sm" />
<SkeletonText size="sm" />
</div>
</div>
{/* Rows */}
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="border-b last:border-b-0 p-4">
<div className="grid grid-cols-4 gap-4 items-center">
<div className="flex items-center space-x-2">
<SkeletonAvatar size="sm" />
<SkeletonText className="w-20" size="sm" />
</div>
<div className="flex justify-center">
<SkeletonText className="w-16" size="sm" />
</div>
<div className="flex justify-center">
<SkeletonText className="w-16" size="sm" />
</div>
<div className="flex justify-center">
<SkeletonButton size="sm" />
</div>
</div>
</div>
))}
</div>
</div>
Custom Variants
Custom Variants
Circle
Secondary
Custom Size
No Shimmer
Slow Animation
<div className="space-y-4">
<div className="space-y-2">
<p className="text-xs text-muted-foreground">Circle</p>
<Skeleton variant="circle" className="w-12 h-12" />
</div>
<div className="space-y-2">
<p className="text-xs text-muted-foreground">Secondary</p>
<Skeleton variant="secondary" className="w-full h-6" />
</div>
<div className="space-y-2">
<p className="text-xs text-muted-foreground">Custom Size</p>
<Skeleton width={200} height={100} />
</div>
<div className="space-y-2">
<p className="text-xs text-muted-foreground">No Shimmer</p>
<Skeleton shimmer={false} className="w-full h-6" />
</div>
</div>
Loading States
Blog Post
<div className="space-y-4 sm:space-y-6">
{/* Header */}
<div className="space-y-3 sm:space-y-4">
<SkeletonText className="w-full sm:w-4/5" size="xl" />
<div className="flex flex-col sm:flex-row items-start sm:items-center space-y-3 sm:space-y-0 sm:space-x-4">
<SkeletonAvatar size="sm" />
<div className="space-y-1">
<SkeletonText className="w-24" size="sm" />
<SkeletonText className="w-20" size="sm" />
</div>
</div>
</div>
{/* Featured Image */}
<SkeletonCard className="h-48 sm:h-64" />
{/* Content */}
<div className="space-y-2 sm:space-y-3">
{Array.from({ length: 6 }).map((_, i) => (
<SkeletonText key={i} className={i === 5 ? "w-3/4" : "w-full"} />
))}
</div>
{/* Tags */}
<div className="flex flex-wrap gap-2">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-6 w-16 rounded-full" />
))}
</div>
</div>
Props
Prop | Type | Default |
---|---|---|
className? | string | undefined |
shimmer? | boolean | true |
duration? | number | 2 |
height? | string | number | undefined |
width? | string | number | undefined |
size? | "sm" | "default" | "lg" | "xl" | "2xl" | "default" |
variant? | "default" | "secondary" | "text" | "circle" | "avatar" | "default" |
SkeletonCard Props
Prop | Type | Default |
---|---|---|
showFooter? | boolean | true |
showHeader? | boolean | true |
showImage? | boolean | true |
Edit on GitHub
Last updated on