Styleframe Logo
Forms

Slider

An input control for selecting a value from a range by dragging a thumb along a track. Composed of a root, track, range, and thumb recipe, with color, size, and orientation options through the recipe system.

Overview

The Slider is an interactive form control used to select a numeric value from a continuous range. It is composed of four recipe parts: useSliderRecipe() for the positioning root, useSliderTrackRecipe() for the neutral rail, useSliderRangeRecipe() for the colored fill, and useSliderThumbRecipe() for the draggable handle. Each composable creates a fully configured recipe — the colored parts share the full palette, while the track stays neutral, mirroring the Progress recipe with an added handle.

The Slider 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 Slider recipe?

The Slider recipe helps you:

  • Ship faster with sensible defaults: Get 9 range and thumb colors, 5 sizes, and 2 orientations out of the box with four composable calls.
  • Compose a structured handle: Four coordinated recipes (root, track, range, thumb) share size and orientation axes, so every part stays internally consistent.
  • Stay accessible by construction: The thumb is built to carry role="slider" semantics, so keyboard and screen-reader support drops straight in.
  • 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, or orientation values at compile time.
  • Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
  • Support dark mode: Track, range, and thumb colors adapt automatically between light and dark color schemes.

Usage

Register the recipes

Add the Slider 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/slider.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import {
    useSliderRecipe,
    useSliderTrackRecipe,
    useSliderRangeRecipe,
    useSliderThumbRecipe,
} from '@styleframe/theme';

const s = styleframe();

const slider = useSliderRecipe(s);
const sliderTrack = useSliderTrackRecipe(s);
const sliderRange = useSliderRangeRecipe(s);
const sliderThumb = useSliderThumbRecipe(s);

export default s;

Build the component

Import the four runtime functions from the virtual module and pass variant props to compute class names. The recipes own the slider's appearance; the current value drives the fill length and thumb offset, which you bind from your model:

src/components/Slider.tsx
import {
    slider,
    sliderTrack,
    sliderRange,
    sliderThumb,
    type SliderProps,
    type SliderRangeProps,
    type SliderThumbProps,
} from "virtual:styleframe";

interface SliderComponentProps extends SliderProps, SliderRangeProps, SliderThumbProps {
    value?: number;
    min?: number;
    max?: number;
    disabled?: boolean;
}

export function Slider({
    color = "primary",
    size = "md",
    orientation = "horizontal",
    value = 50,
    min = 0,
    max = 100,
    disabled = false,
}: SliderComponentProps) {
    const ratio = (value - min) / (max - min);
    const fill = `${Math.min(100, Math.max(0, ratio * 100))}%`;
    const isVertical = orientation === "vertical";

    return (
        <span className={slider({ size, orientation, disabled: String(disabled) })}>
            <span className={sliderTrack({ size, orientation })}>
                <span
                    className={sliderRange({ color, orientation })}
                    style={isVertical ? { height: fill } : { width: fill }}
                />
            </span>
            <span
                className={sliderThumb({ color, size })}
                style={
                    isVertical
                        ? { insetInlineStart: "50%", bottom: fill, transform: "translate(-50%, 50%)" }
                        : { top: "50%", insetInlineStart: fill, transform: "translate(-50%, -50%)" }
                }
                role="slider"
                tabIndex={0}
                aria-valuemin={min}
                aria-valuemax={max}
                aria-valuenow={value}
                aria-orientation={orientation}
                aria-disabled={disabled || undefined}
            />
        </span>
    );
}

See it in action

Colors

The Slider range and thumb recipes include 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 rail behind the fill.

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

Color Reference

Range & thumb colors:

ColorTokenUse Case
primary@color.primaryDefault. Primary brand control
secondary@color.secondarySecondary or supporting control
success@color.successPositive ranges, confirmations
info@color.infoInformational ranges
warning@color.warningCaution states, approaching limits
error@color.errorError states, destructive ranges
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 rail, fixed across color schemes
dark@color.gray-800Dark rail, fixed across color schemes
neutralAdaptiveDefault. Light rail in light mode, dark rail in dark mode
Pro tip: Keep the track neutral and let the range and thumb carry the semantic color. The rail is a background; the fill communicates the value's meaning.

Sizes

Five size variants from xs to xl control the rail thickness (height, or width in vertical orientation) and the thumb diameter. Pass the same size to the root, track, and thumb so the handle and rail stay proportional.

Size Reference

SizeRail ThicknessThumb DiameterUse Case
xs@0.25@0.75Thin, compact controls
sm@0.375@1Dense forms
md@0.5@1.25Default. Standard controls
lg@0.75@1.5Prominent controls
xl@1@2Large, touch-friendly controls
Good to know: The thumb diameter sets the root's cross-axis clearance, so the handle is never clipped by adjacent content.

Orientation

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

Horizontal

The range fills from the start of the track. The root stretches to width: 100% of its container.

Vertical

The range fills from the bottom of the track. The root stretches to height: 100%, so the parent container must have an explicit height for a vertical slider to be visible.

OrientationFill DirectionRoot SizingUse Case
horizontalStart to endwidth: 100%Default. Standard sliders
verticalBottom to topheight: 100%Volume, brightness, level controls

Disabled

The root recipe exposes a disabled variant. When set, it dims the whole control and sets pointer-events: none, which cascades to the track, range, and thumb — you only set it in one place.

slider({ disabled: "true" });
// → "slider ... _opacity:0.5 _cursor:not-allowed _pointer-events:none"
Good to know: Mirror the disabled state to aria-disabled on the thumb so assistive technology announces it, in addition to the recipe's visual treatment.

Anatomy

The Slider recipe is composed of four recipes that work together:

PartRecipeRole
RootuseSliderRecipe()Positioning wrapper. Owns orientation, thumb clearance, and the disabled state
TrackuseSliderTrackRecipe()Neutral rail with overflow: hidden. Holds the range
RangeuseSliderRangeRecipe()Colored fill from the start of the track to the thumb
ThumbuseSliderThumbRecipe()Draggable handle with focus, hover, and active states

The thumb is a sibling of the track, not a child: the track clips its content with overflow: hidden, which would crop the larger handle. The root is position: relative so the thumb can be positioned absolutely along the track.

<!-- All four parts working together -->
<span class="slider(...)">
    <span class="sliderTrack(...)">
        <span class="sliderRange(...)" style="width: 50%"></span>
    </span>
    <span class="sliderThumb(...)" style="inset-inline-start: 50%" role="slider"></span>
</span>
Pro tip: The range and thumb share the same color so the active value reads as one coloured unit. Drive their position from the value with an inline style (or a CSS custom property) — the recipes intentionally leave layout to the data.

Accessibility

  • Use the slider role. Apply role="slider" to the thumb element. This tells assistive technology that the element represents an adjustable value.
<span class="sliderThumb(...)" role="slider" tabindex="0" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></span>
  • Set aria-valuenow, aria-valuemin, and aria-valuemax. These attributes communicate the current value and its bounds to screen readers.
  • Make the thumb focusable and keyboard-operable. Give the thumb tabindex="0" and handle arrow keys (increment/decrement by step), plus Home/End to jump to the minimum and maximum. The recipe's :focus-visible ring makes keyboard focus visible.
<span role="slider" tabindex="0" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" aria-orientation="horizontal"></span>
  • Set aria-orientation for vertical sliders. This tells assistive technology the orientation of the control.
  • Reflect the disabled state. Add aria-disabled="true" to the thumb when the slider is disabled, alongside the recipe's disabled variant.
  • Add a label. Use aria-label or aria-labelledby to give the slider a descriptive name, especially when several sliders appear together.
<label id="volume-label">Volume</label>
<span class="slider(...)">
    <span class="sliderTrack(...)"><span class="sliderRange(...)" style="width: 50%"></span></span>
    <span role="slider" tabindex="0" aria-labelledby="volume-label" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></span>
</span>

Customization

Overriding Defaults

Each slider composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only specify what you want to change:

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

const s = styleframe();

const sliderThumb = useSliderThumbRecipe(s, {
    base: {
        boxShadow: '@box-shadow.md',
    },
    defaultVariants: {
        color: 'neutral',
        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/slider.styleframe.ts
import { styleframe } from 'virtual:styleframe';
import { useSliderRangeRecipe, useSliderThumbRecipe } from '@styleframe/theme';

const s = styleframe();

// Only generate primary and success colors
const sliderRange = useSliderRangeRecipe(s, { filter: { color: ['primary', 'success'] } });
const sliderThumb = useSliderThumbRecipe(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

useSliderRecipe(s, options?)

Creates the slider root recipe — the positioning wrapper that lays out the track and thumb and owns the disabled state.

Variants:

VariantOptionsDefault
orientationhorizontal, verticalhorizontal
sizexs, sm, md, lg, xlmd
disabledtrue, falsefalse

useSliderTrackRecipe(s, options?)

Creates the neutral rail recipe with background color, pill radius, and orientation support.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
orientationhorizontal, verticalhorizontal
sizexs, sm, md, lg, xlmd

useSliderRangeRecipe(s, options?)

Creates the colored fill recipe. Its length is data-driven, so set the fill dimension from the value rather than a variant.

Variants:

VariantOptionsDefault
colorprimary, secondary, success, info, warning, error, light, dark, neutralprimary
orientationhorizontal, verticalhorizontal

useSliderThumbRecipe(s, options?)

Creates the draggable handle recipe with full color support and interactive focus, hover, and active states.

Variants:

VariantOptionsDefault
colorprimary, secondary, success, info, warning, error, light, dark, neutralprimary
sizexs, sm, md, lg, xlmd

Common parameters (all four recipes):

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

Learn more about recipes →

Best Practices

  • Pass size to the root, track, and thumb: All three share the size axis so the rail thickness and handle diameter stay proportional.
  • Keep the track neutral: The rail is a background. Save color for the range and thumb where it communicates the value's meaning.
  • Use the same color for the range and thumb: This makes the filled portion and handle read as a single coloured unit.
  • Drive position from the value: The recipes own appearance, but the range length and thumb offset must be set from the current value via an inline style or a CSS custom property.
  • Disable in one place: Set disabled on the root only — pointer-events: none cascades to every part. Mirror it to aria-disabled on the thumb.
  • Provide an explicit height for vertical sliders: The vertical root uses height: 100%, so the parent container must have an explicit height.
  • Filter what you don't need: If your component only uses a few colors, pass a filter option to reduce generated CSS.

FAQ