Composables

Progress

A progress indicator component with a track and fill bar. Supports multiple colors, sizes, orientations, inverted fill direction, and indeterminate animations through the recipe system.

Overview

The Progress is a visual indicator element used to show the completion status of a task or process. It is composed of two recipe parts: useProgressRecipe() for the track container and useProgressBarRecipe() for the fill bar. Each composable creates a fully configured recipe with color, size, and orientation options — plus compound variants that handle the color-orientation combinations automatically.

The Progress recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.

Why use the Progress recipe?

The Progress recipe helps you:

  • Ship faster with sensible defaults: Get 9 bar colors, 5 sizes, 2 orientations, and 4 indeterminate animations out of the box with two composable calls.
  • Compose structured layouts: Two coordinated recipes (track and bar) share size and orientation axes, so the track and fill stay internally consistent.
  • Animate without extra CSS: Indeterminate animation keyframes are registered automatically when you use the bar recipe — no manual @keyframes definition needed.
  • Maintain consistency: Compound variants ensure every color combination follows the same design rules, including dark mode overrides.
  • 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, size, orientation, or animation 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 recipes

Add the Progress 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/progress.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useProgressRecipe, useProgressBarRecipe } from '@styleframe/theme';

const s = styleframe();

const progress = useProgressRecipe(s);
const progressBar = useProgressBarRecipe(s);

export default s;

Build the component

Import the progress and progressBar runtime functions from the virtual module and pass variant props to compute class names:

src/components/Progress.tsx
import { progress, progressBar } from "virtual:styleframe";

interface ProgressProps {
    color?: "primary" | "secondary" | "success" | "info" | "warning" | "error" | "light" | "dark" | "neutral";
    trackColor?: "light" | "dark" | "neutral";
    size?: "xs" | "sm" | "md" | "lg" | "xl";
    orientation?: "horizontal" | "vertical";
    inverted?: boolean;
    animation?: "none" | "carousel" | "carousel-inverse" | "swing" | "elastic";
    value?: number | null;
}

export function Progress({
    color = "primary",
    trackColor = "neutral",
    size = "md",
    orientation = "horizontal",
    inverted = false,
    animation = "none",
    value = 0,
}: ProgressProps) {
    const isIndeterminate = value == null;
    const clampedValue = isIndeterminate ? 50 : Math.min(100, Math.max(0, value));
    const fillProp = orientation === "vertical" ? "height" : "width";

    return (
        <div
            className={progress({ color: trackColor, size, orientation })}
            role="progressbar"
            aria-valuenow={value ?? undefined}
            aria-valuemin={0}
            aria-valuemax={100}
            aria-orientation={orientation}
        >
            <div
                className={progressBar({
                    color,
                    size,
                    orientation,
                    inverted: String(inverted),
                    animation: isIndeterminate && animation !== "none" ? animation : "none",
                })}
                style={{ [fillProp]: `${clampedValue}%` }}
            />
        </div>
    );
}

See it in action

Colors

The Progress bar recipe includes 9 color variants: the 6 semantic colors (primary, secondary, success, info, warning, error) plus 3 neutral-spectrum colors (light, dark, neutral). The track recipe uses only the 3 neutral-spectrum colors (light, dark, neutral) to provide a subtle background container.

Each color is applied through compound variants with automatic dark mode overrides. The neutral color adapts between light and dark mode automatically.

Color Reference

Bar colors:

ColorTokenUse Case
primary@color.primaryDefault. Primary brand indicator
secondary@color.secondarySecondary or supporting progress
success@color.successSuccessful completions, positive progress
info@color.infoInformational progress, downloads
warning@color.warningCaution states, approaching limits
error@color.errorError states, critical progress
light@color.gray-400Light-themed fill, fixed across color schemes
dark@color.gray-600Dark-themed fill, fixed across color schemes
neutralAdaptiveLight fill in light mode, dark fill in dark mode

Track colors:

ColorTokenUse Case
light@color.gray-200Light track, fixed across color schemes
dark@color.gray-800Dark track, fixed across color schemes
neutralAdaptiveDefault. Light track in light mode, dark track in dark mode
Pro tip: Pass the trackColor separately from the bar color. The track should be a neutral background, while the bar communicates the semantic meaning through color.

Sizes

Five size variants from xs to xl control the height (or width in vertical orientation) and border radius of both the track and bar.

Size Reference

SizeHeight TokenBorder RadiusUse Case
xs@0.25@border-radius.smThin indicators, inline progress
sm@0.375@border-radius.smCompact progress bars
md@0.5@border-radius.mdDefault. Standard progress bars
lg@0.75@border-radius.mdProminent progress indicators
xl@1@border-radius.lgLarge, highly visible progress
Good to know: The size prop must be passed to both the track and bar recipes. In vertical orientation, the size controls the width instead of the height.

Orientation

The orientation variant controls the layout direction of the progress bar. Two options are available: horizontal (default) and vertical.

Horizontal

The bar fills from left to right. The track stretches to width: 100% of its container.

Vertical

The bar fills from bottom to top. The track stretches to height: 100% of its container. The parent container must have an explicit height for the vertical progress bar to be visible.

OrientationFill DirectionTrack SizingUse Case
horizontalLeft to rightwidth: 100%Default. Standard horizontal progress bars
verticalBottom to topheight: 100%Vertical meters, level indicators, volume bars

Inverted

The inverted variant reverses the fill direction of the progress bar. In horizontal orientation, the bar fills from right to left. In vertical orientation, it fills from top to bottom.

OrientationNormalInverted
horizontalLeft → rightRight → left
verticalBottom → topTop → bottom

Indeterminate

When the progress value is null, the bar enters an indeterminate state. The bar displays at 50% width (or height in vertical) and can animate to indicate ongoing work without a known completion percentage.

Four animation styles are available for indeterminate progress bars. The keyframes are registered automatically when the bar recipe is used — no manual @keyframes definition needed.

Animation Reference

AnimationDurationTimingDescription
noneDefault. No animation, static bar at 50%
carousel1.5slinearSlides in the fill direction continuously
carousel-inverse1.5slinearSlides against the fill direction continuously
swing2sease-in-outOscillates back and forth
elastic2sease-in-outStretches and contracts while oscillating

Vertical Indeterminate

Animations adapt automatically for vertical orientation. Horizontal translateX transforms are swapped to translateY, and scaleX to scaleY.

Anatomy

The Progress recipe is composed of two independent recipes that work together:

PartRecipeRole
TrackuseProgressRecipe()Outer container with background color, border radius, and overflow hidden
BaruseProgressBarRecipe()Inner fill element with color, transition, orientation, inversion, and animation

The track provides the neutral background container. The bar is a child element whose width (or height) is controlled by an inline style based on the current value. Both recipes share the size and orientation axes and should receive the same values.

<!-- Both parts working together -->
<div class="progress(...)">
    <div class="progressBar(...)" style="width: 65%"></div>
</div>
Pro tip: You can place multiple bars inside a single track to create a stacked or segmented progress indicator. Each bar independently controls its own color and width.

Accessibility

  • Use the progressbar role. Apply role="progressbar" to the track element. This tells assistive technology that the element represents a progress indicator.
<div role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100" class="...">
    <div class="..." style="width: 65%"></div>
</div>
  • Set aria-valuenow, aria-valuemin, and aria-valuemax. These attributes communicate the current progress value to screen readers. For indeterminate progress, omit aria-valuenow entirely.
<!-- Determinate -->
<div role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">...</div>

<!-- Indeterminate -->
<div role="progressbar" aria-valuemin="0" aria-valuemax="100">...</div>
  • Set aria-orientation for vertical progress bars. This tells assistive technology the orientation of the progress indicator.
<div role="progressbar" aria-orientation="vertical" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">...</div>
  • Add a label. Use aria-label or aria-labelledby to provide a descriptive name for the progress bar, especially when multiple progress bars appear on the same page.
<label id="upload-label">Upload progress</label>
<div role="progressbar" aria-labelledby="upload-label" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100" class="...">
    <div class="..." style="width: 65%"></div>
</div>

Customization

Overriding Defaults

Each progress 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/progress.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useProgressRecipe, useProgressBarRecipe } from '@styleframe/theme';

const s = styleframe();

const progress = useProgressRecipe(s, {
    base: {
        borderRadius: '@border-radius.lg',
    },
    defaultVariants: {
        color: 'neutral',
        orientation: 'horizontal',
        size: 'lg',
    },
});

const progressBar = useProgressBarRecipe(s, {
    defaultVariants: {
        color: 'primary',
        orientation: 'horizontal',
        inverted: 'false',
        animation: 'none',
        size: 'lg',
    },
});

export default s;

Filtering Variants

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

src/components/progress.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useProgressRecipe, useProgressBarRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate primary and success bar colors
const progress = useProgressRecipe(s);
const progressBar = useProgressBarRecipe(s, {
    filter: {
        color: ['primary', 'success'],
    },
});

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

API Reference

useProgressRecipe(s, options?)

Creates the progress track recipe with background color, border radius, and orientation support.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the track
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
colorlight, dark, neutralneutral
orientationhorizontal, verticalhorizontal
sizexs, sm, md, lg, xlmd

useProgressBarRecipe(s, options?)

Creates the progress bar fill recipe with color, orientation, inversion, and indeterminate animation support. Registers 8 keyframes (4 horizontal + 4 vertical) automatically.

Parameters:

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

Variants:

VariantOptionsDefault
colorprimary, secondary, success, info, warning, error, light, dark, neutralprimary
orientationhorizontal, verticalhorizontal
invertedtrue, falsefalse
animationnone, carousel, carousel-inverse, swing, elasticnone
sizexs, sm, md, lg, xlmd

Learn more about recipes →

Best Practices

  • Pass size and orientation to both recipes: The track and bar must share the same size and orientation values so that border radii and layout directions match.
  • Use semantic bar colors for meaning: Use success for completions, error for failures, and primary for general progress — this keeps your UI consistent when tokens change.
  • Use neutral for the track: The track should be a subtle background. Save color for the bar where it communicates progress status.
  • Omit aria-valuenow for indeterminate progress: When value is null, screen readers should not announce a specific percentage. Omit the attribute entirely.
  • Set the fill with inline styles: The recipe handles visual styling, but the bar's width (or height) percentage must be set via an inline style based on the current value.
  • Provide an explicit height for vertical progress: The vertical track uses height: 100%, so the parent container must have an explicit height for the progress bar to be visible.
  • Filter what you don't need: If your component only uses a few colors, pass a filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common variant combination as defaultVariants so component consumers write less code.

FAQ