Skeleton
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
@keyframesdefinition 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:
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:
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"
/>
);
}
<script setup lang="ts">
import { skeleton } from "virtual:styleframe";
const {
size = "md",
rounded = false,
} = defineProps<{
size?: "xs" | "sm" | "md" | "lg" | "xl";
rounded?: boolean;
}>();
</script>
<template>
<div
:class="skeleton({ size, rounded: String(rounded) })"
aria-hidden="true"
/>
</template>
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
| Size | Height Token | Use Case |
|---|---|---|
xs | @0.5 | Single-line metadata, small labels |
sm | @0.75 | Body text lines, descriptions |
md | @1 | Default. Standard content lines |
lg | @1.5 | Headings, larger text blocks |
xl | @2 | Titles, prominent content areas |
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.
| Value | Border Radius | Use Case |
|---|---|---|
false | @border-radius.md | Default. Rectangular placeholders for text and content blocks |
true | @border-radius.full | Circular or pill-shaped placeholders for avatars and icons |
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.
| Property | Value | Token |
|---|---|---|
| Animation name | skeleton-pulse | — |
| Duration | 2s | — |
| Timing function | ease-in-out | @easing.ease-in-out |
| Iteration count | infinite | — |
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 witharia-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:
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:
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;
API Reference
useSkeletonRecipe(s, options?)
Creates a skeleton loading placeholder recipe with a pulse animation, size variants, and a rounded option.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the skeleton |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
size | xs, sm, md, lg, xl | md |
rounded | true, false | false |
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
roundedfor avatar placeholders: Combineroundedwith 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
filteroption to reduce generated CSS. - Override defaults at the recipe level: Set your most common size as
defaultVariantsso 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
@color.gray-200 in light mode and @color.gray-800 in dark mode to blend with any surrounding content. Adding color variants would imply semantic meaning (success, error, etc.) that doesn't apply to loading states. If you need a different background color, override the base.background property in the options.skeleton-pulse keyframes animation during setup. It cycles the element's opacity between 1 (at 0% and 100%) and 0.5 (at 50%) over 2 seconds, using an ease-in-out timing function. The animation runs infinitely. The keyframes are registered automatically when you call useSkeletonRecipe() — no manual @keyframes definition is needed.Override the animation properties in the base option. To disable the animation entirely, set animationName to none. To change the speed, override animationDuration:
const skeleton = useSkeletonRecipe(s, {
base: {
animationDuration: '1s', // Faster pulse
},
});
The size variant controls the height. Set the width using utility classes on the element itself. For example, to simulate a text line of a specific width:
<div class="skeleton({ size: 'sm' }) _width:[200px]"></div>
For a circular avatar placeholder, combine rounded with equal width and height:
<div class="skeleton({ size: 'xl', rounded: 'true' }) _width:3 _height:3"></div>
@color.gray-200, @border-radius.md, and @easing.ease-in-out through string refs. These tokens need to be defined in your Styleframe instance for the recipe to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.rounded variant uses "true" and "false" as string keys internally, but your component can accept a boolean prop and convert it with String(rounded) when passing it to the recipe function.Modal
A dialog component for focused interactions, composed of overlay, container, header, body, and footer sections. Supports multiple colors, visual styles, and sizes through the recipe system.
Tooltip
A floating label component for supplementary information, composed of a content bubble and directional arrow. Supports multiple colors, visual styles, and sizes through the recipe system.