Fiber UI LogoFiberUI

Skeleton

Placeholder loading states for content that is loading.

Basic Usage

"use client";

import { Skeleton } from "@repo/ui/components/skeleton";
import { SkeletonWrapper } from "./skeleton-wrapper";

export const Example1 = () => {
    return (
        <SkeletonWrapper>
            <div className="flex items-center gap-4">
                <Skeleton variant="circular" className="h-12 w-12" />
                <div className="flex-1 space-y-2">
                    <Skeleton variant="text" width="50%" />
                    <Skeleton variant="text" width="30%" size="xs" />
                </div>
            </div>
            <Skeleton variant="default" height={120} />
            <div className="space-y-2">
                <Skeleton variant="text" />
                <Skeleton variant="text" />
                <Skeleton variant="text" width="60%" />
            </div>
        </SkeletonWrapper>
    );
};

Sizes

XS
SM
MD
LG
XL
"use client";

import { Skeleton } from "@repo/ui/components/skeleton";
import { SkeletonWrapper } from "./skeleton-wrapper";

export const Example2 = () => {
    return (
        <SkeletonWrapper>
            <div className="flex items-center gap-4">
                <span className="text-muted-foreground w-8 text-sm">XS</span>
                <Skeleton size="xs" className="w-full" />
            </div>
            <div className="flex items-center gap-4">
                <span className="text-muted-foreground w-8 text-sm">SM</span>
                <Skeleton size="sm" className="w-full" />
            </div>
            <div className="flex items-center gap-4">
                <span className="text-muted-foreground w-8 text-sm">MD</span>
                <Skeleton size="md" className="w-full" />
            </div>
            <div className="flex items-center gap-4">
                <span className="text-muted-foreground w-8 text-sm">LG</span>
                <Skeleton size="lg" className="w-full" />
            </div>
            <div className="flex items-center gap-4">
                <span className="text-muted-foreground w-8 text-sm">XL</span>
                <Skeleton size="xl" className="w-full" />
            </div>
        </SkeletonWrapper>
    );
};

Animations

Pulse (default)

Wave

None

"use client";

import { Skeleton } from "@repo/ui/components/skeleton";
import { SkeletonWrapper } from "./skeleton-wrapper";

export const Example3 = () => {
    return (
        <SkeletonWrapper>
            <div>
                <p className="text-muted-foreground mb-2 text-sm">
                    Pulse (default)
                </p>
                <Skeleton animation="pulse" height={60} />
            </div>
            <div>
                <p className="text-muted-foreground mb-2 text-sm">Wave</p>
                <Skeleton animation="wave" height={60} />
            </div>
            <div>
                <p className="text-muted-foreground mb-2 text-sm">None</p>
                <Skeleton animation="none" height={60} />
            </div>
        </SkeletonWrapper>
    );
};

Card Skeleton

"use client";

import { SkeletonCard } from "@repo/ui/components/skeleton";
import { SkeletonWrapper } from "./skeleton-wrapper";

export const Example4 = () => {
    return (
        <SkeletonWrapper className="grid w-full gap-4 sm:grid-cols-2">
            <SkeletonCard />
            <SkeletonCard animation="wave" />
        </SkeletonWrapper>
    );
};

Profile Skeleton

"use client";

import {
    Skeleton,
    SkeletonAvatar,
    SkeletonText,
    SkeletonButton,
} from "@repo/ui/components/skeleton";
import { SkeletonWrapper } from "./skeleton-wrapper";

export const Example5 = () => {
    return (
        <SkeletonWrapper>
            <div className="border-border w-full rounded-lg border p-6">
                <div className="flex items-start gap-4">
                    <SkeletonAvatar avatarSize="xl" />
                    <div className="flex-1">
                        <Skeleton variant="text" width="40%" className="mb-1" />
                        <Skeleton
                            variant="text"
                            width="25%"
                            size="xs"
                            className="mb-4"
                        />
                        <SkeletonText lines={2} />
                        <div className="mt-4 flex gap-2">
                            <SkeletonButton buttonSize="sm" />
                            <SkeletonButton buttonSize="sm" />
                        </div>
                    </div>
                </div>
            </div>
        </SkeletonWrapper>
    );
};

Data Loading

"use client";

import { useState, useEffect } from "react";
import { Skeleton, SkeletonAvatar } from "@repo/ui/components/skeleton";
import { SkeletonWrapper } from "./skeleton-wrapper";

interface User {
    name: string;
    email: string;
    avatar: string;
}

const mockUsers: User[] = [
    { name: "Alice Johnson", email: "alice@example.com", avatar: "AJ" },
    { name: "Bob Smith", email: "bob@example.com", avatar: "BS" },
    { name: "Carol Williams", email: "carol@example.com", avatar: "CW" },
];

export const Example6 = () => {
    const [loading, setLoading] = useState(true);
    const [users, setUsers] = useState<User[]>([]);

    useEffect(() => {
        const timer = setTimeout(() => {
            setUsers(mockUsers);
            setLoading(false);
        }, 2000);
        return () => clearTimeout(timer);
    }, []);

    const handleReload = () => {
        setLoading(true);
        setUsers([]);
        setTimeout(() => {
            setUsers(mockUsers);
            setLoading(false);
        }, 2000);
    };

    return (
        <SkeletonWrapper>
            <div className="w-full space-y-4">
                <button
                    onClick={handleReload}
                    className="bg-primary text-primary-foreground rounded-lg px-4 py-2 text-sm"
                >
                    Reload Data
                </button>
                <div className="divide-border divide-y">
                    {loading
                        ? Array.from({ length: 3 }).map((_, i) => (
                              <div
                                  key={i}
                                  className="flex items-center gap-3 py-3"
                              >
                                  <SkeletonAvatar avatarSize="md" />
                                  <div className="flex-1 space-y-1">
                                      <Skeleton variant="text" width="30%" />
                                      <Skeleton
                                          variant="text"
                                          width="50%"
                                          size="xs"
                                      />
                                  </div>
                              </div>
                          ))
                        : users.map((user) => (
                              <div
                                  key={user.email}
                                  className="flex items-center gap-3 py-3"
                              >
                                  <div className="bg-primary text-primary-foreground flex h-10 w-10 items-center justify-center rounded-full text-sm font-medium">
                                      {user.avatar}
                                  </div>
                                  <div>
                                      <p className="font-medium">{user.name}</p>
                                      <p className="text-muted-foreground text-sm">
                                          {user.email}
                                      </p>
                                  </div>
                              </div>
                          ))}
                </div>
            </div>
        </SkeletonWrapper>
    );
};

Component Code

"use client";

import { forwardRef } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "tailwind-variants";

export const skeletonVariants = cva("bg-muted relative overflow-hidden", {
    variants: {
        variant: {
            default: "rounded-md",
            circular: "rounded-full",
            text: "rounded h-4 w-full",
            button: "rounded-lg h-9",
            avatar: "rounded-full aspect-square",
            card: "rounded-lg",
        },
        size: {
            xs: "h-2",
            sm: "h-4",
            md: "h-6",
            lg: "h-8",
            xl: "h-12",
            full: "h-full w-full",
        },
        animation: {
            pulse: "animate-pulse",
            wave: "skeleton-wave",
            none: "",
        },
    },
    defaultVariants: {
        variant: "default",
        size: "md",
        animation: "pulse",
    },
});

interface SkeletonProps
    extends React.HTMLAttributes<HTMLDivElement>,
        VariantProps<typeof skeletonVariants> {
    /** Width of the skeleton (CSS value) */
    width?: string | number;
    /** Height of the skeleton (CSS value) */
    height?: string | number;
}

const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
    (
        { className, variant, size, animation, width, height, style, ...props },
        ref,
    ) => {
        const computedStyle = {
            ...style,
            width:
                width !== undefined
                    ? typeof width === "number"
                        ? `${width}px`
                        : width
                    : undefined,
            height:
                height !== undefined
                    ? typeof height === "number"
                        ? `${height}px`
                        : height
                    : undefined,
        };

        return (
            <>
                {animation === "wave" && (
                    <style>{`
                        @keyframes skeleton-wave-animation {
                            0% { transform: translateX(-100%); }
                            100% { transform: translateX(100%); }
                        }
                        .skeleton-wave::after {
                            content: "";
                            position: absolute;
                            inset: 0;
                            background: linear-gradient(
                                90deg,
                                transparent,
                                rgba(255, 255, 255, 0.15),
                                transparent
                            );
                            animation: skeleton-wave-animation 1.5s ease-in-out infinite;
                        }
                    `}</style>
                )}
                <div
                    ref={ref}
                    role="status"
                    aria-busy="true"
                    aria-label="Loading..."
                    className={cn(
                        skeletonVariants({ variant, size, animation }),
                        className,
                    )}
                    style={computedStyle}
                    {...props}
                />
            </>
        );
    },
);
Skeleton.displayName = "Skeleton";

interface SkeletonTextProps extends Omit<SkeletonProps, "variant"> {
    /** Number of text lines to render */
    lines?: number;
    /** Gap between lines */
    gap?: string;
}

const SkeletonText = forwardRef<HTMLDivElement, SkeletonTextProps>(
    ({ lines = 3, gap = "0.5rem", className, ...props }, ref) => {
        return (
            <div
                ref={ref}
                className={cn("flex flex-col", className)}
                style={{ gap }}
            >
                {Array.from({ length: lines }).map((_, i) => (
                    <Skeleton
                        key={i}
                        variant="text"
                        width={i === lines - 1 ? "60%" : "100%"}
                        {...props}
                    />
                ))}
            </div>
        );
    },
);
SkeletonText.displayName = "SkeletonText";

interface SkeletonAvatarProps extends Omit<SkeletonProps, "variant"> {
    /** Size of the avatar */
    avatarSize?: "sm" | "md" | "lg" | "xl";
}

const avatarSizes = {
    sm: "h-8 w-8",
    md: "h-10 w-10",
    lg: "h-12 w-12",
    xl: "h-16 w-16",
};

const SkeletonAvatar = forwardRef<HTMLDivElement, SkeletonAvatarProps>(
    ({ avatarSize = "md", className, ...props }, ref) => {
        return (
            <Skeleton
                ref={ref}
                variant="circular"
                size="full"
                className={cn(avatarSizes[avatarSize], className)}
                {...props}
            />
        );
    },
);
SkeletonAvatar.displayName = "SkeletonAvatar";

interface SkeletonButtonProps extends Omit<SkeletonProps, "variant"> {
    /** Size of the button skeleton */
    buttonSize?: "sm" | "md" | "lg";
}

const buttonSizes = {
    sm: "h-8 w-20",
    md: "h-9 w-24",
    lg: "h-11 w-32",
};

const SkeletonButton = forwardRef<HTMLDivElement, SkeletonButtonProps>(
    ({ buttonSize = "md", className, ...props }, ref) => {
        return (
            <Skeleton
                ref={ref}
                variant="button"
                size="full"
                className={cn(buttonSizes[buttonSize], className)}
                {...props}
            />
        );
    },
);
SkeletonButton.displayName = "SkeletonButton";

interface SkeletonCardProps extends Omit<SkeletonProps, "variant"> {
    /** Show header placeholder */
    showHeader?: boolean;
    /** Show image placeholder */
    showImage?: boolean;
    /** Show action buttons */
    showActions?: boolean;
}

const SkeletonCard = forwardRef<HTMLDivElement, SkeletonCardProps>(
    (
        {
            showHeader = true,
            showImage = true,
            showActions = true,
            className,
            animation,
            ...props
        },
        ref,
    ) => {
        return (
            <div
                ref={ref}
                className={cn("border-border rounded-lg border p-4", className)}
                {...props}
            >
                {showHeader && (
                    <div className="mb-4 flex items-center gap-3">
                        <SkeletonAvatar avatarSize="md" animation={animation} />
                        <div className="flex-1 space-y-2">
                            <Skeleton
                                variant="text"
                                width="40%"
                                animation={animation}
                            />
                            <Skeleton
                                variant="text"
                                width="25%"
                                size="xs"
                                animation={animation}
                            />
                        </div>
                    </div>
                )}
                {showImage && (
                    <Skeleton
                        variant="card"
                        className="mb-4 aspect-video w-full"
                        animation={animation}
                    />
                )}
                <SkeletonText lines={3} animation={animation} />
                {showActions && (
                    <div className="mt-4 flex gap-2">
                        <SkeletonButton buttonSize="sm" animation={animation} />
                        <SkeletonButton buttonSize="sm" animation={animation} />
                    </div>
                )}
            </div>
        );
    },
);
SkeletonCard.displayName = "SkeletonCard";

export {
    Skeleton,
    SkeletonText,
    SkeletonAvatar,
    SkeletonButton,
    SkeletonCard,
    type SkeletonProps,
    type SkeletonTextProps,
    type SkeletonAvatarProps,
    type SkeletonButtonProps,
    type SkeletonCardProps,
};