Slider
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:
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:
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:
| Color | Token | Use Case |
|---|---|---|
primary | @color.primary | Default. Primary brand control |
secondary | @color.secondary | Secondary or supporting control |
success | @color.success | Positive ranges, confirmations |
info | @color.info | Informational ranges |
warning | @color.warning | Caution states, approaching limits |
error | @color.error | Error states, destructive ranges |
light | @color.gray-400 | Light-themed fill, fixed across color schemes |
dark | @color.gray-600 | Dark-themed fill, fixed across color schemes |
neutral | Adaptive | Light fill in light mode, dark fill in dark mode |
Track colors:
| Color | Token | Use Case |
|---|---|---|
light | @color.gray-200 | Light rail, fixed across color schemes |
dark | @color.gray-800 | Dark rail, fixed across color schemes |
neutral | Adaptive | Default. Light rail in light mode, dark rail in dark mode |
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
| Size | Rail Thickness | Thumb Diameter | Use Case |
|---|---|---|---|
xs | @0.25 | @0.75 | Thin, compact controls |
sm | @0.375 | @1 | Dense forms |
md | @0.5 | @1.25 | Default. Standard controls |
lg | @0.75 | @1.5 | Prominent controls |
xl | @1 | @2 | Large, touch-friendly controls |
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.
| Orientation | Fill Direction | Root Sizing | Use Case |
|---|---|---|---|
horizontal | Start to end | width: 100% | Default. Standard sliders |
vertical | Bottom to top | height: 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"
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:
| Part | Recipe | Role |
|---|---|---|
| Root | useSliderRecipe() | Positioning wrapper. Owns orientation, thumb clearance, and the disabled state |
| Track | useSliderTrackRecipe() | Neutral rail with overflow: hidden. Holds the range |
| Range | useSliderRangeRecipe() | Colored fill from the start of the track to the thumb |
| Thumb | useSliderThumbRecipe() | 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>
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
sliderrole. Applyrole="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, andaria-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), plusHome/Endto jump to the minimum and maximum. The recipe's:focus-visiblering makes keyboard focus visible.
<span role="slider" tabindex="0" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" aria-orientation="horizontal"></span>
- Set
aria-orientationfor 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'sdisabledvariant. - Add a label. Use
aria-labeloraria-labelledbyto 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:
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:
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;
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:
| Variant | Options | Default |
|---|---|---|
orientation | horizontal, vertical | horizontal |
size | xs, sm, md, lg, xl | md |
disabled | true, false | false |
useSliderTrackRecipe(s, options?)
Creates the neutral rail recipe with background color, pill radius, and orientation support.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
orientation | horizontal, vertical | horizontal |
size | xs, sm, md, lg, xl | md |
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:
| Variant | Options | Default |
|---|---|---|
color | primary, secondary, success, info, warning, error, light, dark, neutral | primary |
orientation | horizontal, vertical | horizontal |
useSliderThumbRecipe(s, options?)
Creates the draggable handle recipe with full color support and interactive focus, hover, and active states.
Variants:
| Variant | Options | Default |
|---|---|---|
color | primary, secondary, success, info, warning, error, light, dark, neutral | primary |
size | xs, sm, md, lg, xl | md |
Common parameters (all four recipes):
| 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 |
Best Practices
- Pass
sizeto the root, track, and thumb: All three share thesizeaxis 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
colorfor 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
disabledon the root only —pointer-events: nonecascades to every part. Mirror it toaria-disabledon 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
filteroption to reduce generated CSS.
FAQ
overflow: hidden, the range carries semantic color, and the thumb adds interactive focus and hover states. Four recipes give each part its own variants while sharing the size and orientation axes.overflow: hidden so the range fill is clipped to the rail's rounded corners. The thumb is larger than the rail, so placing it inside the track would crop it. Instead, the thumb is a sibling positioned absolutely against the position: relative root.width (or height in vertical) and the thumb's inset-inline-start (or bottom) from it with an inline style or a CSS custom property. See the Usage example above.light always uses the same gray tones regardless of the color scheme. dark always uses darker gray tones. neutral adapts to the current color scheme: it appears light in light mode and dark in dark mode. Use neutral when you want the slider to blend naturally with the surrounding interface.role="slider", tabindex="0", and aria-valuemin/aria-valuemax/aria-valuenow. Handle arrow keys to change the value by your step, and Home/End to jump to the bounds. The recipe's :focus-visible ring makes keyboard focus visible automatically.disabled variant uses "true" and "false" as string keys internally, but your component can accept a boolean prop and convert it with String(disabled) when passing it to the recipe function.@color.primary, @border-radius.full, and @box-shadow.sm through string refs. These tokens need to be defined in your Styleframe instance for the recipes to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.Select
A multi-select form control composed of a trigger, a floating listbox panel, selectable options, dismissable value chips, a chevron indicator, group labels, and separators. Supports light, dark, and neutral colors, per-part visual styles, and three sizes through the recipe system.
Switch
A switch built on the native checkbox input, with a sliding knob, on, disabled, and focus states, light, dark, and neutral track colors, and three sizes through the recipe system.