Build websites 10x faster with HextaUI Blocks — Learn more
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.

app/global.css
@theme {
  --animate-shimmer: shimmer 1.5s infinite linear;
}

@keyframes shimmer {
    0% {
        transform: translateX(-100%);
    }
    100% {
        transform: translateX(100%);
    }
}
tailwind.config.js
  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.

components/ui/skeleton.tsx
"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

PropTypeDefault
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

PropTypeDefault
showFooter?
boolean
true
showHeader?
boolean
true
showImage?
boolean
true
Edit on GitHub

Last updated on