Spinner
Overview
The Spinner is a visual indicator used to communicate that an action is in progress. The useSpinnerRecipe() composable creates a fully configured recipe with color and size options, while useSpinnerCircleRecipe() handles the SVG spinner animation and useSpinnerOverlayRecipe() provides an optional backdrop overlay.
The Spinner 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 Spinner recipe?
The Spinner recipe helps you:
- Ship faster with sensible defaults: Get 4 colors and 3 sizes out of the box with a single composable call.
- Maintain consistency: The SVG-based spinner animation is consistent across all color and size combinations.
- 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 color or size values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipes
Add the Spinner recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import { useSpinnerRecipe, useSpinnerCircleRecipe, useSpinnerOverlayRecipe } from '@styleframe/theme';
const s = styleframe();
const spinner = useSpinnerRecipe(s);
const spinnerCircle = useSpinnerCircleRecipe(s);
const spinnerOverlay = useSpinnerOverlayRecipe(s);
export default s;
Build the component
Import the spinner and spinnerCircle runtime functions from the virtual module and pass variant props to compute class names:
import { spinner, spinnerCircle } from "virtual:styleframe";
interface SpinnerProps {
color?: "primary" | "light" | "dark" | "neutral";
size?: "auto" | "sm" | "md" | "lg";
label?: string;
children?: React.ReactNode;
}
export function Spinner({
color = "primary",
size = "md",
label,
children,
}: SpinnerProps) {
const containerClasses = spinner({ color, size });
const circleClasses = spinnerCircle({ size });
return (
<div className={containerClasses} role="status">
<svg className={circleClasses} viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" />
</svg>
{label && <span>{label}</span>}
{children}
</div>
);
}
<script setup lang="ts">
import { spinner, spinnerCircle } from "virtual:styleframe";
const { color = "primary", size = "md", label } = defineProps<{
color?: "primary" | "light" | "dark" | "neutral";
size?: "auto" | "sm" | "md" | "lg";
label?: string;
}>();
</script>
<template>
<div :class="spinner({ color, size })" role="status">
<svg :class="spinnerCircle({ size })" viewBox="0 0 50 50">
<circle cx="25" cy="25" r="20" />
</svg>
<span v-if="label">{{ label }}</span>
<slot />
</div>
</template>
See it in action
Colors
The Spinner supports 4 color options. The color variant sets the CSS color property on the container, which cascades to the SVG spinner via currentColor.
Color Reference
| Color | Token | Use Case |
|---|---|---|
primary | @color.primary | Default loading state, primary actions |
light | @color.gray-700 | Loading on dark backgrounds, stays fixed in dark mode |
dark | @color.white | Loading on light backgrounds, stays fixed in dark mode |
neutral | Adaptive (light ↔ dark) | Default adaptive color, adjusts to the current color scheme |
neutral for general-purpose loading states that need to adapt to both light and dark mode automatically.Sizes
The Spinner comes in 3 sizes that control both the spinner dimensions and the label text size.
| Size | Spinner Dimensions | Font Size |
|---|---|---|
auto | 100% × 100% | Inherits base |
sm | @2 × @2 | @font-size.xs |
md | @3 × @3 | @font-size.sm |
lg | @4 × @4 | @font-size.md |
Label
You can display optional text below the spinner using the label prop.
Overlay
Use useSpinnerOverlayRecipe() to create a fixed overlay backdrop for full-page or container-level loading states.
<script setup lang="ts">
import { spinnerOverlay } from "virtual:styleframe";
</script>
<template>
<div :class="spinnerOverlay({})">
<Spinner color="dark" size="lg" label="Loading..." />
</div>
</template>
Accessibility
- Always include
role="status"on the spinner container to announce loading state to screen readers. - Provide descriptive text via the
labelprop or a visually hidden<span>for screen readers when no visible label is shown. - Animation respects
prefers-reduced-motion: The design tokens preset automatically disables animations when the user has reduced motion enabled.
Customization
Overriding Defaults
const spinner = useSpinnerRecipe(s, {
defaultVariants: { color: 'neutral', size: 'lg' },
});
Filtering Variants
const spinner = useSpinnerRecipe(s, {
filter: {
color: ['primary', 'neutral'],
},
});
API Reference
useSpinnerRecipe(s, options?)
Creates the spinner container recipe with color and size variants.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles |
options.variants | Variants | Custom variant definitions |
options.defaultVariants | Record<keyof Variants, string> | Default variant values |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | primary, light, dark, neutral | primary |
size | auto, sm, md, lg | md |
useSpinnerCircleRecipe(s, options?)
Creates the SVG spinner recipe with size variants and animation keyframes.
Variants:
| Variant | Options | Default |
|---|---|---|
size | auto, sm, md, lg | md |
useSpinnerOverlayRecipe(s, options?)
Creates the overlay backdrop recipe. No variants — provides a fixed full-screen backdrop.
Best Practices
- Use
role="status"on the spinner element for accessibility. - Provide a label for longer loading operations so users know what's happening.
- Choose the right color: Use
primaryfor brand-colored spinners,neutralfor general use, andlight/darkfor specific background contexts. - Filter what you don't need: Pass a
filteroption to reduce generated CSS. - Use the overlay sparingly: Full-page overlays block interaction — prefer inline spinners when possible.
FAQ
spinner-rotate for continuous rotation and spinner-dash for the stroke dash effect that creates the "chasing" appearance. These are registered automatically when you call useSpinnerCircleRecipe().useSpinnerRecipe sets the CSS color property via compound variants. The SVG circle's stroke is set to currentColor via a selector in the spinner circle recipe's setup function. This means the spinner color automatically inherits from the container — no extra wiring needed.filter option, compound variants that reference filtered-out values are automatically removed. Default variants are also adjusted if they reference a removed value.Yes! Override the overlay's position to absolute and ensure the parent container has position: relative:
const spinnerOverlay = useSpinnerOverlayRecipe(s, {
base: { position: 'absolute' },
});
Chip
A status indicator component positioned at the corner of an element. Supports multiple colors, visual styles, sizes, positions, and inset mode through the recipe system.
Overview
Explore Styleframe's utility composables for generating CSS utility classes. Create flexible, reusable styling primitives with full type safety.