Composables

Spinner

A loading spinner component with color, size, and optional overlay — built as a multi-part recipe system with SVG-based animation.

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:

src/components/spinner.styleframe.ts
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:

src/components/Spinner.tsx
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>
    );
}

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

ColorTokenUse Case
primary@color.primaryDefault loading state, primary actions
light@color.gray-700Loading on dark backgrounds, stays fixed in dark mode
dark@color.whiteLoading on light backgrounds, stays fixed in dark mode
neutralAdaptive (light ↔ dark)Default adaptive color, adjusts to the current color scheme
Pro tip: Use 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.

SizeSpinner DimensionsFont Size
auto100% × 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.

src/components/SpinnerOverlay.vue
<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 label prop 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

src/components/spinner.styleframe.ts
const spinner = useSpinnerRecipe(s, {
    defaultVariants: { color: 'neutral', size: 'lg' },
});

Filtering Variants

src/components/spinner.styleframe.ts
const spinner = useSpinnerRecipe(s, {
    filter: {
        color: ['primary', 'neutral'],
    },
});
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useSpinnerRecipe(s, options?)

Creates the spinner container recipe with color and size variants.

Parameters:

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

Variants:

VariantOptionsDefault
colorprimary, light, dark, neutralprimary
sizeauto, sm, md, lgmd

useSpinnerCircleRecipe(s, options?)

Creates the SVG spinner recipe with size variants and animation keyframes.

Variants:

VariantOptionsDefault
sizeauto, sm, md, lgmd

useSpinnerOverlayRecipe(s, options?)

Creates the overlay backdrop recipe. No variants — provides a fixed full-screen backdrop.

Learn more about recipes ->

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 primary for brand-colored spinners, neutral for general use, and light/dark for specific background contexts.
  • Filter what you don't need: Pass a filter option to reduce generated CSS.
  • Use the overlay sparingly: Full-page overlays block interaction — prefer inline spinners when possible.

FAQ