Composables

Skeleton

A loading placeholder component that displays a pulsing gray block to indicate content is being loaded. Supports multiple sizes and a rounded option through the recipe system.

Overview

The Skeleton is a loading placeholder element used to indicate that content is being fetched or processed. The useSkeletonRecipe() composable creates a fully configured recipe with size and rounded options, plus a built-in pulse animation via keyframes — no additional CSS required.

The Skeleton recipe integrates directly with the default design tokens preset and generates type-safe utility classes at build time with zero runtime CSS.

Why use the Skeleton recipe?

The Skeleton recipe helps you:

  • Ship faster with sensible defaults: Get 5 sizes and a rounded option out of the box with a single composable call.
  • Animate without extra CSS: The pulse keyframes animation is registered automatically when you use the recipe — no manual @keyframes definition needed.
  • Maintain consistency: All skeleton placeholders share the same animation timing, colors, and border radius across your application.
  • Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
  • Stay type-safe: Full TypeScript support means your editor catches invalid size values at compile time.
  • Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
  • Support dark mode: Background colors adapt automatically between light and dark color schemes.

Usage

Register the recipe

Add the Skeleton recipe to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipe itself:

src/components/skeleton.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useSkeletonRecipe } from '@styleframe/theme';

const s = styleframe();

const skeleton = useSkeletonRecipe(s);

export default s;

Build the component

Import the skeleton runtime function from the virtual module and pass variant props to compute class names:

src/components/Skeleton.tsx
import { skeleton } from "virtual:styleframe";

interface SkeletonProps {
    size?: "xs" | "sm" | "md" | "lg" | "xl";
    rounded?: boolean;
    className?: string;
}

export function Skeleton({
    size = "md",
    rounded = false,
    className,
}: SkeletonProps) {
    return (
        <div
            className={`${skeleton({ size, rounded: String(rounded) })} ${className ?? ""}`}
            aria-hidden="true"
        />
    );
}

See it in action

Sizes

Five size variants from xs to xl control the height of the skeleton placeholder. The width is not set by the recipe — use utility classes or CSS to control it based on the content being loaded.

Size Reference

SizeHeight TokenUse Case
xs@0.5Single-line metadata, small labels
sm@0.75Body text lines, descriptions
md@1Default. Standard content lines
lg@1.5Headings, larger text blocks
xl@2Titles, prominent content areas
Good to know: The size variant only controls the height. Set the width using utility classes like _width:[250px] or _width:full to match the shape of the content being loaded.

Rounded

The rounded variant applies a fully circular border radius (@border-radius.full), turning the skeleton into a pill or circle shape. This is useful for placeholder avatars and circular icons.

ValueBorder RadiusUse Case
false@border-radius.mdDefault. Rectangular placeholders for text and content blocks
true@border-radius.fullCircular or pill-shaped placeholders for avatars and icons
Pro tip: Combine rounded with equal width and height utility classes to create a perfect circle placeholder: <Skeleton size="xl" :rounded="true" class="_width:3 _height:3" />.

Animation

The Skeleton recipe includes a built-in skeleton-pulse keyframes animation that fades the element between full and half opacity on a 2-second loop. The keyframes are registered automatically when the recipe is used — no additional setup is needed.

PropertyValueToken
Animation nameskeleton-pulse
Duration2s
Timing functionease-in-out@easing.ease-in-out
Iteration countinfinite

The skeleton-pulse keyframes cycle between opacity: 1 at 0% and 100%, and opacity: 0.5 at 50%.

Accessibility

  • Hide skeleton placeholders from screen readers. Skeleton elements are purely visual and carry no meaningful content. Add aria-hidden="true" to each skeleton element so assistive technology ignores them.
<!-- Correct: hidden from assistive technology -->
<div class="..." aria-hidden="true"></div>
  • Mark the loading container with aria-busy. Wrap skeleton placeholders in a container with aria-busy="true" while loading, and remove it when real content appears. This tells screen readers the region is updating.
<!-- While loading -->
<div aria-busy="true">
    <div class="..." aria-hidden="true"></div>
    <div class="..." aria-hidden="true"></div>
</div>

<!-- After loading -->
<div>
    <h2>Actual content</h2>
    <p>Real description text.</p>
</div>
  • Provide a screen-reader-only loading message. Since skeleton elements are hidden, add a visually hidden text element that announces the loading state to assistive technology.
<div aria-busy="true">
    <span class="_sr-only">Loading content...</span>
    <div class="..." aria-hidden="true"></div>
</div>

Customization

Overriding Defaults

The useSkeletonRecipe() composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:

src/components/skeleton.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useSkeletonRecipe } from '@styleframe/theme';

const s = styleframe();

const skeleton = useSkeletonRecipe(s, {
    base: {
        borderRadius: '@border-radius.lg',
        animationDuration: '1.5s',
    },
    defaultVariants: {
        size: 'sm',
        rounded: 'false',
    },
});

export default s;

Filtering Variants

If you only need a subset of the available sizes, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:

src/components/skeleton.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useSkeletonRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate sm and md sizes
const skeleton = useSkeletonRecipe(s, {
    filter: {
        size: ['sm', 'md'],
    },
});

export default s;
Good to know: Filtering adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useSkeletonRecipe(s, options?)

Creates a skeleton loading placeholder recipe with a pulse animation, size variants, and a rounded option.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the skeleton
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
sizexs, sm, md, lg, xlmd
roundedtrue, falsefalse

Learn more about recipes →

Best Practices

  • Match the shape of real content: Size and position skeleton elements to approximate the layout of the content they replace. This reduces layout shift when content loads.
  • Use rounded for avatar placeholders: Combine rounded with equal width and height to create circular placeholders that match avatar shapes.
  • Set width with utility classes: The recipe controls height through size, but width should be set per-instance to match the expected content width.
  • Group skeletons in a container: Wrap related skeleton elements together and use aria-busy="true" on the container for accessibility.
  • Filter what you don't need: If your component only uses a few sizes, pass a filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common size as defaultVariants so component consumers write less code.
  • Avoid animating too fast: The default 2-second pulse is designed to feel natural. Faster animations can feel aggressive and slower ones can seem broken.

FAQ