Installation
AutoSkeleton ships as a single npm package with zero required dependencies (React 16.8+ is a peer dep).
npm install @gyojiro/autoskeleton-react
# or
pnpm add @gyojiro/autoskeleton-react
# or
yarn add @gyojiro/autoskeleton-reactSetup
Import the bundled stylesheet once at the top level of your app. The CSS file contains the keyframe animations and CSS custom properties used internally.
Next.js App Router
// app/layout.tsx
import "@gyojiro/autoskeleton-react/style.css";
import type { Metadata } from "next";
export const metadata: Metadata = { title: "My App" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}Vite / Create React App
// main.tsx (or index.tsx)
import "@gyojiro/autoskeleton-react/style.css";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);SkeletonProvider — but it is completely optional. Every skeleton works standalone with sensible defaults./components, and /examples use Tailwind CSS classes (flex, gap-4, card, etc.) for the surrounding layout markup. The skeleton component calls themselves don't require Tailwind — they work with any styling setup — but if you copy a snippet wholesale and don't have Tailwind installed, the wrapper divs won't be laid out as shown; swap those classes for your own CSS, or add Tailwind to match exactly.Quick Start
Import any component and drop it where the real content will go. No provider needed.
import {
AvatarSkeleton,
TextSkeleton,
ButtonSkeleton,
CardSkeleton,
} from "@gyojiro/autoskeleton-react";
// Inline composition
function UserCardSkeleton() {
return (
<div className="flex gap-3 p-4">
<AvatarSkeleton size={48} />
<div className="flex-1">
<TextSkeleton lines={2} />
</div>
</div>
);
}
// Or use a pre-built composite
function LoadingState() {
return <CardSkeleton />;
}Live preview
Real-World Pattern
The key to pixel-perfect skeleton UIs is to share the same container markup in both the loading and loaded states.
import { useState, useEffect } from "react";
import { AvatarSkeleton, TextSkeleton, ButtonSkeleton } from "@gyojiro/autoskeleton-react";
function UserProfile() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
// Shared card shell — used in BOTH branches
const card = "flex flex-col gap-4 p-6 rounded-xl border bg-white";
if (!user) {
return (
<div className={card}>
<AvatarSkeleton size={64} />
<TextSkeleton lines={3} />
<ButtonSkeleton width="100%" height={40} />
</div>
);
}
return (
<div className={card}>
<img src={user.avatar} className="w-16 h-16 rounded-full" alt={user.name} />
<div>
<p className="font-semibold">{user.name}</p>
<p className="text-sm text-slate-500">{user.bio}</p>
</div>
<button className="w-full h-10 rounded-lg bg-blue-600 text-white">Follow</button>
</div>
);
}<AvatarSkeleton size={64} /> when the real avatar is w-16 h-16 (64px), lineHeight={28} when the title is 1.75rem, and height={40} for a h-10 button. See the Examples page for full walkthroughs.Theming
All theme values flow through React Context. Wrap once with SkeletonProvider to configure every skeleton below it, or pass theme props directly to SkeletonGroup for local overrides.
Default theme
const DEFAULT_THEME = {
animation: "wave", // "wave" | "pulse" | "fade" | "none"
duration: 1.2, // seconds
easing: "ease-in-out", // any CSS timing function
animationDirection: "normal",// "normal" | "reverse" | "alternate" | "alternate-reverse"
radius: "md", // "none" | "sm" | "md" | "lg" | "full" | string
color: "#E5E7EB", // base background
highlight: "#F9FAFB", // shimmer highlight (wave animation)
};SkeletonProvider
Pass any subset of theme props. Unspecified values fall back to the defaults above.
import { SkeletonProvider } from "@gyojiro/autoskeleton-react";
// Slower pulse instead of wave
<SkeletonProvider animation="pulse" duration={1.8}>
<App />
</SkeletonProvider>
// Custom brand colors
<SkeletonProvider color="#E0E7FF" highlight="#EEF2FF">
<App />
</SkeletonProvider>
// Reverse wave direction
<SkeletonProvider animationDirection="reverse">
<App />
</SkeletonProvider>
// Cubic-bezier easing
<SkeletonProvider easing="cubic-bezier(0.4, 0, 0.2, 1)">
<App />
</SkeletonProvider>Animation types
| Value | Description |
|---|---|
| wave | Shimmer sweep from left to right (default) |
| pulse | Gentle opacity in / out pulse |
| fade | Soft fade in and out |
| none | Static placeholder — no animation |
animationDirection
Maps directly to the CSS animation-directionproperty. Useful for creating a “back and forth” shimmer effect.
// Default — shimmer left to right
<SkeletonProvider animationDirection="normal">…</SkeletonProvider>
// Shimmer right to left
<SkeletonProvider animationDirection="reverse">…</SkeletonProvider>
// Alternating — great for subtle pulse-like waves
<SkeletonProvider animationDirection="alternate">…</SkeletonProvider>
<SkeletonProvider animationDirection="alternate-reverse">…</SkeletonProvider>CSS custom properties
You can also override theme values at the CSS level using these custom properties. This is useful for dark-mode overrides via a CSS media query.
/* globals.css */
:root {
--skeleton-color: #E5E7EB;
--skeleton-highlight: #F9FAFB;
--skeleton-duration: 1.2s;
--skeleton-easing: ease-in-out;
--skeleton-direction: normal;
}
@media (prefers-color-scheme: dark) {
:root {
--skeleton-color: #374151;
--skeleton-highlight: #4B5563;
}
}Dark Theme
The package exports a DARK_THEME preset that overrides the two color values to match dark backgrounds.
import { SkeletonProvider, DARK_THEME } from "@gyojiro/autoskeleton-react";
// DARK_THEME = { color: "#374151", highlight: "#4B5563" }
// Spread into SkeletonProvider
<SkeletonProvider {...DARK_THEME}>
<ProfileSkeleton />
</SkeletonProvider>
// Conditionally apply based on app theme state
const { isDark } = useTheme();
<SkeletonProvider {...(isDark ? DARK_THEME : {})}>
<App />
</SkeletonProvider>Live dark / light toggle
Layout: Flex & Grid
SkeletonGroup arranges children with flexbox by default. A row next to a fixed-size element (like an avatar) fills the remaining space automatically — no manual flex: 1 needed.
import { SkeletonGroup, AvatarSkeleton, TextSkeleton } from "@gyojiro/autoskeleton-react";
<SkeletonGroup direction="row" gap={12} align="center">
<AvatarSkeleton size={48} />
<TextSkeleton lines={2} />
</SkeletonGroup>Live preview
Grid
Set layout="grid" for CSS grid instead of flexbox. columns renders repeat(columns, 1fr) — that many equal-width tracks — or pass a raw grid-template-columns string for full control.
<SkeletonGroup layout="grid" columns={3} gap={16}>
<Skeleton height={80} radius="md" />
<Skeleton height={80} radius="md" />
<Skeleton height={80} radius="md" />
</SkeletonGroup>Responsive columns & direction
columns and direction both accept a { base, sm, md, lg, xl }object instead of a constant value, resolved via a CSS container query scoped to the group's own rendered width — not the viewport. A grid nested inside a narrow sidebar or modal responds to that container's width correctly, the same way it would at the edge of the browser window.
// 1 column by default, 2 from a 480px container width, 3 from 640px
<SkeletonGroup layout="grid" columns={{ base: 1, sm: 2, md: 3 }} gap={16}>
{items.map((item) => <ProductCardSkeleton key={item.id} />)}
</SkeletonGroup>sm = 480, md = 640, lg = 800, xl= 1024. Resize this browser window to see the grid above respond — it's reacting to its own container, not the page.Local Overrides with SkeletonGroup
SkeletonGroup doubles as a layout wrapper and a local theme scope. Any theme props passed to it override only its descendants — the rest of the tree is unaffected.
import { SkeletonGroup, CardSkeleton, TextSkeleton } from "@gyojiro/autoskeleton-react";
// Global provider uses "wave"; this section uses "pulse"
<SkeletonProvider animation="wave">
<TextSkeleton lines={3} />
<SkeletonGroup animation="pulse" color="#DBEAFE" highlight="#EFF6FF">
<CardSkeleton />
<CardSkeleton />
</SkeletonGroup>
</SkeletonProvider>Live preview
Outer — wave (default)
SkeletonGroup override — pulse
Accessibility
AutoSkeleton follows WAI-ARIA guidelines for loading indicators.
aria-label
By default every skeleton is decorative (aria-hidden="true"). Pass an aria-label to expose it to screen readers with role="status".
// Decorative (default) — hidden from screen readers
<CardSkeleton />
// Announced — screen reader says "Loading product card..."
<CardSkeleton aria-label="Loading product card..." />
// Announce the whole section once instead of each piece
<div role="status" aria-label="Loading user profile...">
<AvatarSkeleton />
<TextSkeleton lines={2} />
</div>aria-busy
SkeletonGroup renders aria-busy="true" by default when an aria-label is provided. Set it to false to suppress this.
<SkeletonGroup aria-label="Loading profile..." aria-busy={true}>
<AvatarSkeleton />
<TextSkeleton lines={3} />
</SkeletonGroup>prefers-reduced-motion
The bundled stylesheet automatically disables all CSS animations when the user has requested reduced motion via the OS accessibility setting. No configuration needed — it works out of the box.
/* Already handled inside @gyojiro/autoskeleton-react/style.css */
@media (prefers-reduced-motion: reduce) {
[data-skeleton] {
animation: none;
}
}animation="none" programmatically on any skeleton to force a static placeholder regardless of user preference.Best practices
- Announce the loading region once using a wrapper
<div role="status">rather than on every individual skeleton. - Remove or hide the skeleton container (not just swap content) so screen readers are notified the loading state ended.
- Use
aria-labeltext that describes what is loading, not the visual shape (e.g. “Loading user profile” not “skeleton rectangle”). - Prefer
animation="pulse"oranimation="none"for content that will take a long time to load — the wave shimmer can feel distracting after a few seconds.
API Reference
Full props for every component are documented on the Components page with live previews, searchable props tables, and copy-paste code examples.
Exports
| Export | Kind | Description |
|---|---|---|
| Skeleton | Component | Core primitive rectangle/circle block |
| SkeletonGroup | Component | Flex layout wrapper + local theme scope |
| SkeletonProvider | Component | Global theme context provider |
| TextSkeleton | Component | Multi-line paragraph placeholder |
| AvatarSkeleton | Component | Circular avatar placeholder |
| ButtonSkeleton | Component | Rounded button placeholder |
| ImageSkeleton | Component | Aspect-ratio-aware image placeholder |
| ArticleSkeleton | Component | Hero + author + body layout |
| CardSkeleton | Component | Versatile card (column or row) |
| ChartSkeleton | Component | Bar, line, or donut chart placeholder |
| ChatMessageSkeleton | Component | Chat bubbles + input area |
| CommentSkeleton | Component | Stacked comment thread |
| DashboardSkeleton | Component | Stats + chart + table layout |
| FormSkeleton | Component | Labeled fields + submit button |
| GallerySkeleton | Component | CSS-grid image gallery |
| ListSkeleton | Component | Icon + text list items |
| MediaObjectSkeleton | Component | Media block beside text |
| NavbarSkeleton | Component | Logo + links + actions bar |
| PricingCardSkeleton | Component | Pricing tier card |
| ProductCardSkeleton | Component | E-commerce product card |
| ProfileSkeleton | Component | Social profile layout |
| SidebarSkeleton | Component | App sidebar navigation |
| StatisticCardSkeleton | Component | KPI / stat card |
| StoriesBarSkeleton | Component | Horizontally-scrolling avatar row |
| TableSkeleton | Component | Tabular data placeholder |
| TimelineSkeleton | Component | Vertical timeline |
| DARK_THEME | Constant | { color: '#374151', highlight: '#4B5563' } |
| useSkeleton | Hook | Returns the current SkeletonTheme from context |
| ResponsiveValue<T> | Type | T | { base, sm, md, lg, xl } — for SkeletonGroup's columns/direction |
| SkeletonBreakpoint | Type | "sm" | "md" | "lg" | "xl" |
useSkeleton hook
Reads the current SkeletonTheme from context — useful for building custom skeletons that respect the global theme.
import { useSkeleton } from "@gyojiro/autoskeleton-react";
function MyCustomSkeleton() {
const theme = useSkeleton();
// theme.color, theme.animation, theme.duration, etc.
return (
<div
style={{
width: 200,
height: 20,
background: theme.color,
borderRadius: 4,
}}
/>
);
}TypeScript types
import type {
SkeletonTheme, // Full theme config interface
SkeletonAnimation, // "wave" | "pulse" | "fade" | "none"
SkeletonAnimationDirection, // "normal" | "reverse" | "alternate" | "alternate-reverse"
SkeletonRadius, // "none" | "sm" | "md" | "lg" | "full" | string
SkeletonVariant, // "default" | "rounded" | "circle"
} from "@gyojiro/autoskeleton-react";