Slider
Usage
25%
50%
75%
onChange value: 35onChangeEnd value: 35
"use client";
import { useState } from "react";
import { Slider } from "@/ui/slider";
import { Svg } from "@/ui/svg";
import { Typography } from "@/ui/typography";
import { useApp as useAppContext } from "@/config/app-context";
const INITIAL_VALUES: number = 35;
const marks = [
{ value: 41, label: "25%" },
{ value: 66, label: "50%" },
{ value: 91, label: "75%" }
];
export function SliderDemo() {
const ctx = useAppContext();
const [value, setValue] = useState<number>(INITIAL_VALUES);
const [endValue, setEndValue] = useState<number>(INITIAL_VALUES);
return (
<div className="flex size-full max-w-96 flex-col items-center justify-center [&_svg]:mr-auto [&_svg]:rtl:ml-auto [&_svg]:rtl:mr-0">
<Slider dir={ctx.dir} value={value} onChange={setValue} onChangeEnd={setEndValue} marks={marks} min={16} max={116} />
<hr className="mt-14 w-full border-t border-border" />
<Typography prose="span">
onChange value: <b>{value}</b>
</Typography>
<Svg size={value} currentFill="fill" role="img" fill="#149eca">
<circle cx="12" cy="12" r="2.1" />
<path d="m19.62,8.19c-.19-.09-.39-.17-.59-.25.03-.22.06-.43.08-.64.24-2.38-.33-4.09-1.61-4.83-1.28-.74-3.05-.38-4.99,1.02-.17.12-.34.25-.51.39-.17-.14-.34-.27-.51-.39-1.94-1.4-3.71-1.76-4.99-1.02-1.28.74-1.85,2.45-1.61,4.83.02.21.05.42.08.64-.2.08-.4.16-.59.25-2.18.98-3.38,2.33-3.38,3.81s1.2,2.83,3.38,3.81c.19.09.39.17.59.25-.03.22-.06.43-.08.64-.24,2.38.33,4.09,1.61,4.83.43.25.92.37,1.46.37,1.04,0,2.25-.47,3.53-1.39.17-.12.34-.25.51-.39.17.14.34.27.51.39,1.28.92,2.49,1.39,3.53,1.39.53,0,1.02-.12,1.46-.37,1.28-.74,1.85-2.45,1.61-4.83-.02-.21-.05-.42-.08-.64.2-.08.4-.16.59-.25,2.18-.98,3.38-2.33,3.38-3.81s-1.2-2.83-3.38-3.81Zm-6.5-3.85c1.09-.79,2.1-1.19,2.92-1.19.35,0,.67.08.94.23.89.51,1.29,1.9,1.09,3.82-.01.13-.03.26-.05.39-.96-.29-2-.51-3.1-.65-.67-.88-1.38-1.68-2.11-2.36.11-.08.21-.16.32-.24Zm-6.36,8.98c.22.43.45.87.7,1.3.25.43.51.85.78,1.25-.73-.13-1.43-.3-2.07-.5.15-.66.35-1.35.6-2.05Zm-.6-4.69c.65-.2,1.34-.37,2.07-.5-.27.4-.53.82-.78,1.25-.25.43-.48.86-.7,1.3-.25-.7-.46-1.38-.6-2.05Zm1.14,3.37c.31-.7.67-1.4,1.07-2.1.4-.69.83-1.36,1.28-1.97.76-.08,1.55-.12,2.35-.12s1.59.04,2.35.12c.45.62.88,1.28,1.28,1.97.4.69.76,1.4,1.07,2.1-.31.7-.67,1.4-1.07,2.1-.4.69-.83,1.36-1.28,1.97-.76.08-1.55.12-2.35.12s-1.59-.04-2.35-.12c-.45-.62-.88-1.28-1.28-1.97-.4-.69-.76-1.4-1.07-2.1Zm9.24,2.62c.25-.43.48-.86.7-1.3.25.7.46,1.38.6,2.05-.65.2-1.34.37-2.07.5.27-.4.53-.82.78-1.25Zm.7-3.94c-.22-.43-.45-.87-.7-1.3-.25-.43-.51-.85-.78-1.25.73.13,1.43.3,2.07.5-.15.66-.35,1.35-.6,2.05Zm-5.23-5.42c.5.46.99.98,1.47,1.55-.48-.03-.98-.05-1.47-.05s-.99.02-1.47.05c.48-.57.97-1.09,1.47-1.55Zm-4.98-1.88c.27-.15.58-.23.94-.23.81,0,1.83.41,2.92,1.19.11.08.21.16.32.24-.73.68-1.44,1.48-2.11,2.36-1.1.14-2.15.36-3.1.65-.02-.13-.03-.26-.05-.39-.19-1.91.21-3.3,1.09-3.82Zm-2.22,11.47c-1.75-.79-2.76-1.83-2.76-2.86s1.01-2.07,2.76-2.86c.12-.05.24-.11.37-.16.22.97.56,1.99.99,3.01-.43,1.02-.76,2.04-.99,3.01-.12-.05-.25-.1-.37-.16Zm6.07,4.8c-1.56,1.12-2.96,1.47-3.85.96-.89-.51-1.29-1.9-1.09-3.82.01-.13.03-.26.05-.39.96.29,2,.51,3.1.65.67.88,1.38,1.68,2.11,2.36-.11.08-.21.16-.32.24Zm1.12-.92c-.5-.46-.99-.98-1.47-1.55.48.03.98.05,1.47.05s.99-.02,1.47-.05c-.48.57-.97,1.09-1.47,1.55Zm4.98,1.88c-.89.51-2.29.16-3.85-.96-.11-.08-.21-.16-.32-.24.73-.68,1.44-1.48,2.11-2.36,1.1-.14,2.15-.36,3.1-.65.02.13.03.26.05.39.19,1.91-.21,3.3-1.09,3.82Zm2.22-5.76c-.12.05-.24.11-.37.16-.22-.97-.56-1.99-.99-3.01.43-1.02.76-2.04.99-3.01.12.05.25.1.37.16,1.75.79,2.76,1.83,2.76,2.86s-1.01,2.07-2.76,2.86Z" />
</Svg>
<Typography prose="span">
onChangeEnd value: <b>{endValue}</b>
</Typography>
<Svg size={endValue} currentFill="fill" role="img">
<path d="m11.61,1c-.16,0-.28,0-.33,0-.05,0-.2.02-.33.03-3.12.28-6.05,1.97-7.91,4.56-1.03,1.44-1.69,3.07-1.94,4.81-.09.6-.1.78-.1,1.6s.01,1,.1,1.6c.6,4.13,3.54,7.6,7.52,8.89.71.23,1.47.39,2.32.48.33.04,1.77.04,2.11,0,1.48-.16,2.73-.53,3.96-1.16.19-.1.23-.12.2-.14-.02-.01-.82-1.09-1.79-2.4l-1.76-2.38-2.2-3.26c-1.21-1.79-2.21-3.26-2.22-3.26,0,0-.02,1.45-.02,3.22,0,3.1,0,3.22-.05,3.3-.06.11-.1.15-.19.2-.07.03-.13.04-.45.04h-.37l-.1-.06c-.06-.04-.11-.09-.14-.16l-.05-.1v-4.31s.01-4.31.01-4.31l.07-.08s.11-.1.16-.13c.09-.04.12-.05.49-.05.44,0,.51.02.63.14.03.03,1.23,1.83,2.65,4,1.45,2.19,2.89,4.38,4.34,6.57l1.74,2.64.09-.06c.78-.51,1.61-1.23,2.26-1.98,1.39-1.6,2.29-3.55,2.59-5.62.09-.6.1-.78.1-1.6s-.01-1-.1-1.6c-.6-4.13-3.54-7.6-7.52-8.89-.7-.23-1.45-.38-2.29-.48-.15-.02-.99-.03-1.48-.03h0Zm3.73,6.62c.32,0,.37,0,.45.04.1.05.19.15.22.25.02.06.02,1.25.02,3.95v3.87s-.69-1.05-.69-1.05l-.68-1.05v-2.81c0-1.82,0-2.84.02-2.89.03-.12.11-.22.21-.27.09-.05.12-.05.46-.05h0Z" />
</Svg>
</div>
);
}
Properties
25%
50%
75%
Color
Size
Round
"use client";
import React from "react";
import { Slider } from "@/ui/slider";
import { Typography } from "@/ui/typography";
import { useApp as useAppContext } from"@/config/app-context";
const marks = [
{ value: 25, label: "25%" },
{ value: 50, label: "50%" },
{ value: 75, label: "75%" }
];
export function SliderDemo() {
const ctx = useAppContext();
const [value, setValue] = React.useState(35);
const [endValue, setEndValue] = React.useState(35);
return (
<div className="flex size-full max-w-96 flex-col items-center justify-center">
<Slider
round={32}
dir={ctx.dir}
defaultValue={35}
value={value}
onChange={setValue}
onChangeEnd={setEndValue}
marks={marks}
/>
<Typography prose="span">
onChange value: <b>{value}</b>
</Typography>
<Typography prose="span">
onChangeEnd value: <b>{endValue}</b>
</Typography>
</div>
);
}
Slider Inverted
import { RangeSlider, Slider } from '@/ui/slider';
export function SliderInvertedDemo() {
return (
<div className="flex size-full max-w-96 flex-col items-center justify-center gap-10">
<Slider inverted defaultValue={80} />
<RangeSlider inverted defaultValue={[40, 80]} />
</div>
);
}
Slider Label
No labelFormatted labelLabel always visibleCustom label transition
40
"use client";
import React from "react";
import { Slider } from "@/ui/slider";
import { useApp as useAppContext } from "@/config/app-context";
import { Typography } from "@/ui/typography";
export function SliderLabelDemo() {
const ctx = useAppContext();
return (
<div className="mb-12 flex size-full max-w-96 flex-col items-center justify-center [&>span:not(:first-of-type)]:mt-6">
<Typography prose="span">No label</Typography>
<Slider dir={ctx.dir} defaultValue={40} label={null} />
<Typography prose="span">Formatted label</Typography>
<Slider dir={ctx.dir} defaultValue={40} label={(value) => `${value} °C`} />
<Typography prose="span">Label always visible</Typography>
<Slider dir={ctx.dir} defaultValue={40} labelAlwaysOn />
<Typography prose="span">Custom label transition</Typography>
<Slider dir={ctx.dir} defaultValue={40} labelTransitionProps={{ transition: "skew-down", duration: 150, timingFunction: "linear" }} />
</div>
);
}
Slider Marks
MarksRestrict selection to marksDisabled
xs
sm
md
lg
xl
20%
50%
80%
xs
sm
md
lg
xl
import { Slider, RangeSlider } from '@/ui/slider';
import { Typography } from "@/ui/typography";
const marks = [
{ value: 0, label: "xs" },
{ value: 25, label: "sm" },
{ value: 50, label: "md" },
{ value: 75, label: "lg" },
{ value: 100, label: "xl" }
];
export function SliderMarksDemo() {
return (
<div className="m-auto mb-12 flex size-full flex-col items-center justify-center [&>*]:max-w-96">
<Typography prose="span">Marks</Typography>
<Slider defaultValue={40} marks={[{ value: 10 }, { value: 40 }, { value: 95 }]} />
<Slider defaultValue={40} marks={marks} className="mt-6" />
<RangeSlider defaultValue={[20, 80]} marks={[{ value: 20, label: "20%" }, { value: 50, label: "50%" }, { value: 80, label: "80%" }]} className="mt-6" />
<Typography prose="span" className="mt-10">Restrict selection to marks</Typography>
<Slider restrictToMarks defaultValue={25} marks={Array.from({ length: 5 }).map((_, index) => ({ value: index * 25 }))} />
<Typography prose="span" className="mt-6">Disabled</Typography>
<Slider defaultValue={60} disabled />
<RangeSlider disabled defaultValue={[25, 75]} marks={marks} className="mt-6" />
</div>
);
}
Slider Scale
1 MB
1 MB
1 GB
import { RangeSlider, Slider } from '@/ui/slider';
const getScale = (v: number) => 2 ** v;
function valueLabelFormat(value: number) {
const units = ["KB", "MB", "GB", "TB"];
let unitIndex = 0;
let scaledValue = value;
while (scaledValue >= 1024 && unitIndex < units.length - 1) {
unitIndex += 1;
scaledValue /= 1024;
}
return `${scaledValue} ${units[unitIndex]}`;
}
export function SliderScaleDemo() {
return (
<div className="flex size-full max-w-96 flex-col items-center justify-center gap-12 py-8">
<Slider scale={getScale} step={1} min={2} max={30} labelAlwaysOn defaultValue={10} label={valueLabelFormat} />
<RangeSlider scale={getScale} step={1} min={2} max={30} labelAlwaysOn defaultValue={[10, 20]} label={valueLabelFormat} />
</div>
);
}
Slider Step
Decimal Values
Decimal stepStep matched with marks
xs
sm
md
lg
xl
import { Slider, RangeSlider } from '@/ui/slider';
import { Typography } from "@/ui/typography";
const marks = [
{ value: 0, label: "xs" },
{ value: 25, label: "sm" },
{ value: 50, label: "md" },
{ value: 75, label: "lg" },
{ value: 100, label: "xl" }
];
export function SliderStepDemo() {
return (
<div className="mb-12 flex size-full max-w-96 flex-col items-center justify-center">
<Typography prose="span">Decimal Values</Typography>
<Slider min={0} max={1} step={0.0005} defaultValue={0.5535} />
<RangeSlider minRange={0.2} min={0} max={1} step={0.0005} defaultValue={[0.1245, 0.5535]} />
<Typography prose="span" className="mt-6">Decimal step</Typography>
<Slider defaultValue={0} min={-10} max={10} label={(value) => value.toFixed(1)} step={0.1} />
<Typography prose="span" className="mt-6">Step matched with marks</Typography>
<Slider
defaultValue={50}
label={(val) => marks.find((mark) => mark.value === val)!.label}
step={25}
marks={marks}
/>
</div>
);
}
Slider Thumb
Thumb size
import { RangeSlider, Slider } from "@/ui/slider";
import { Typography } from "@/ui/typography";
import { MoonStarIcon, StarIcon, SunIcon } from "@/icons/*";
export function SliderThumbDemo() {
const styles = { thumb: { borderWidth: "2px", padding: "3px" } };
return (
<div className="mb-12 flex size-full max-w-96 flex-col items-center justify-center">
<Slider thumbSize={26} defaultValue={20} />
<Typography prose="span" className="mt-6">Thumb Icon</Typography>
<Slider thumbSize={26} thumbChildren={<StarIcon size="75%" />} color="#f08c00" label={null} defaultValue={40} styles={styles} />
<RangeSlider thumbSize={26} color="red" label={null} defaultValue={[20, 60]} thumbChildren={[<SunIcon size="75%" key="1" />, <MoonStarIcon size="75%" key="2" />]} styles={styles} className="mt-6" />
</div>
);
}
API References
Styles API
type T = "root" | "label" | "thumb" | "trackContainer" | "track" | "bar" | "markWrapper" | "mark" | "markLabel";
Styles API | Type | Default | Annotation |
---|---|---|---|
unstyled? | Partial<Record<T, boolean>> | false | if true , default styles will be removed |
className? | string | undefined | pass to root component <div> |
classNames? | Partial<Record<T, string>> | undefined | |
style? | CSSProperties | undefined | pass to root component <div> |
styles? | Partial<Record<T, CSSProperties>> | undefined |
Props API
Props API | Type | Default | Annotation |
---|---|---|---|
dir? | "ltr" | "rtl" | ltr | Type of direction slider |
color? | CSSProperties["color"] | hsl(var(--constructive)) | Key of valid CSS color, controls color of track and thumb |
round? | number | string | 999 | Key of valid CSS value to set border-radius , numbers are converted to rem |
size? | (string & {}) | number | 12 | Controls size of the track |
min? | number | 0 | Minimal possible value |
max? | number | 100 | Maximum possible value |
step? | number | 1 | Number by which value will be incremented/decremented with thumb drag and arrows |
precision? | number | Number of significant digits after the decimal point | |
value? | number | Controlled component value | |
defaultValue? | number | Uncontrolled component default value | |
onChange? | (value: number) => void | Called when value changes | |
onChangeEnd? | (value: number) => void | Called when user stops dragging slider or changes value with arrows | |
name? | string | Hidden input name, use with uncontrolled component | |
marks? | { value: number; label?: React.ReactNode }[] | Marks displayed on the track | |
label? | React.ReactNode | ((value: number) => React.ReactNode) | Function to generate label or any react node to render instead, set to null to disable label | |
labelTransitionProps? | TransitionOverride | 0 | Props passed down to the Transition component, { transition: 'fade', duration: 0 } |
labelAlwaysOn? | boolean | false | Determines whether the label should be visible when the slider is not being dragged or hovered |
thumbLabel? | string | Thumb aria-label | |
showLabelOnHover? | boolean | true | Determines whether the label should be displayed when the slider is hovered |
thumbChildren? | React.ReactNode | Content rendered inside thumb | |
disabled? | boolean | false | Disables slider |
thumbSize? | number | string | size | Thumb width and height , by default value is computed based on size prop |
scale? | (value: number) => number | A transformation function to change the scale of the slider | |
inverted? | boolean | false | Determines whether track value representation should be inverted |
hiddenInputProps? | React.ComponentPropsWithoutRef<"input"> | Props passed down to the hidden input | |
restrictToMarks? | boolean | false | Determines whether the selection should be only allowed from the given marks array |
thumbProps? | React.ComponentPropsWithoutRef<"div"> | Props passed down to thumb element |
Source Codes
"use client";
import * as React from "react";
import { clamp, useMove } from "@/hooks/use-move";
import { Transition, TransitionOverride } from "@/hooks/use-dialog";
import { useUncontrolled } from "@/hooks/use-uncontrolled";
import { cn, cvx, rem, type inferType, type cvxProps } from "cretex";
import { useMergedRef } from "@/hooks/use-merged-ref";
const classes = cvx({
variants: {
selector: {
root: "stylelayer-slider",
label: "slider-label",
thumb: "slider-thumb",
trackContainer: "slider-track-container",
track: "slider-track",
bar: "slider-bar",
markWrapper: "slider-mark-wrapper",
mark: "slider-mark",
markLabel: "slider-mark-label"
}
}
});
type __Selector = NonNullable<cvxProps<typeof classes>["selector"]>;
type Options = StylesNames<__Selector> &
__SliderProps & {
// variant?: string;
};
type CSSProperties = React.CSSProperties & { [key: string]: any };
type NestedRecord<U extends [string, unknown], T extends string> = {
[K in U as K[0]]?: Partial<Record<T, K[1]>>;
};
type Styles = ["unstyled", boolean] | ["classNames", string] | ["styles", CSSProperties];
type StylesNames<T extends string, Exclude extends string = never> = Omit<NestedRecord<Styles, T> & { className?: string; style?: CSSProperties }, Exclude>;
type ComponentProps<T extends React.ElementType, Exclude extends string = never> = StylesNames<__Selector> &
React.PropsWithoutRef<Omit<React.ComponentProps<T>, "style" | "color" | Exclude>> & { color?: CSSProperties["color"] };
type CtxProps = {
dir: "ltr" | "rtl";
getStyles(selector: __Selector, options?: Options): inferType<typeof getStyles>;
};
function getStyles(selector: __Selector, options: Options = {}) {
const { size = 8, color = "hsl(var(--constructive))", thumbSize, round = 999, unstyled, className, classNames, style, styles, disabled, inverted } = options;
return {
"aria-disabled": disabled ? "true" : undefined,
"data-disabled": disabled ? "true" : undefined,
"data-inverted": inverted ? "true" : undefined,
className: cn(!unstyled?.[selector] && classes({ selector }), classNames?.[selector], className),
style: {
...(selector === "root"
? {
"--slider-size": rem(size),
"--slider-label-fz": "0.8125rem",
"--slider-color": color,
"--slider-thumb-size": thumbSize !== undefined ? rem(thumbSize) : "calc(var(--slider-size) * 2)",
"--slider-round": rem(round),
"--slider-track-bg": "hsl(var(--muted-foreground))",
"--slider-track-disabled-bg": "hsl(var(--muted))"
}
: undefined),
pointerEvents: disabled ? "none" : undefined,
...styles?.[selector],
...style
} as CSSProperties
};
}
const ctx = React.createContext<CtxProps | undefined>(undefined);
const useSlider = () => React.useContext(ctx)!;
export interface __SliderProps {
inverted?: boolean;
disabled?: boolean;
size?: (string & {}) | number;
round?: (string & {}) | number;
thumbSize?: string | number;
color?: CSSProperties["color"];
}
export interface SliderProps extends __SliderProps, ComponentProps<"div", "value" | "onChange"> {
min?: number;
max?: number;
step?: number;
precision?: number;
name?: string;
marks?: { value: number; label?: React.ReactNode }[];
label?: React.ReactNode | ((value: number) => React.ReactNode);
labelTransitionProps?: TransitionOverride;
labelAlwaysOn?: boolean;
thumbLabel?: string;
showLabelOnHover?: boolean;
thumbChildren?: React.ReactNode;
hiddenInputProps?: React.ComponentPropsWithoutRef<"input">;
restrictToMarks?: boolean;
thumbProps?: React.ComponentPropsWithoutRef<"div">;
dir?: "ltr" | "rtl";
value?: number;
defaultValue?: number;
onChange?: (value: number) => void;
scale?: (value: number) => number;
onChangeEnd?: (value: number) => void;
}
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>((_props, ref) => {
const {
classNames,
styles,
value,
onChange,
onChangeEnd,
size,
defaultValue,
name,
thumbChildren,
unstyled,
inverted,
hiddenInputProps,
restrictToMarks,
thumbProps,
round,
min = 0,
max = 100,
step = 1,
marks = [],
label = f => f,
precision: _precision,
dir = "ltr",
labelTransitionProps = { transition: "fade", duration: 0 },
labelAlwaysOn = false,
thumbLabel = "",
showLabelOnHover = true,
disabled = false,
scale = v => v,
...props
} = _props;
const [hovered, setHovered] = React.useState(false);
const [_value, setValue] = useUncontrolled({
value: typeof value === "number" ? clamp(value, min!, max!) : value,
defaultValue: typeof defaultValue === "number" ? clamp(defaultValue, min!, max!) : defaultValue,
finalValue: clamp(0, min!, max!),
onChange
});
const valueRef = React.useRef(_value);
const root = React.useRef<HTMLDivElement>(null);
const thumb = React.useRef<HTMLDivElement>(null);
const position = getPosition({ value: _value, min: min!, max: max! });
const scaledValue = scale!(_value);
const _label = typeof label === "function" ? label(scaledValue) : label;
const precision = _precision ?? getPrecision(step!);
const handleChange = React.useCallback(
({ x }: { x: number }) => {
if (!disabled) {
const nextValue = getChangeValue({
value: x,
min: min!,
max: max!,
step: step!,
precision
});
setValue(
restrictToMarks && marks?.length
? findClosestNumber(
nextValue,
marks.map(mark => mark.value)
)
: nextValue
);
valueRef.current = nextValue;
}
},
[disabled, min, max, step, precision, setValue, marks, restrictToMarks]
);
const { ref: container, active } = useMove(
handleChange,
{
onScrubEnd: () =>
onChangeEnd?.(
restrictToMarks && marks?.length
? findClosestNumber(
valueRef.current,
marks.map(mark => mark.value)
)
: valueRef.current
)
},
dir
);
const handleTrackKeydownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!disabled) {
switch (event.key) {
case "ArrowUp": {
event.preventDefault();
thumb.current?.focus();
if (restrictToMarks && marks) {
const nextValue = getNextMarkValue(_value, marks);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
const nextValue = getFloatingValue(Math.min(Math.max(_value + step!, min!), max!), precision);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
case "ArrowRight": {
event.preventDefault();
thumb.current?.focus();
if (restrictToMarks && marks) {
const nextValue = dir === "rtl" ? getPreviousMarkValue(_value, marks) : getNextMarkValue(_value, marks);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
const nextValue = getFloatingValue(Math.min(Math.max(dir === "rtl" ? _value - step! : _value + step!, min!), max!), precision);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
case "ArrowDown": {
event.preventDefault();
thumb.current?.focus();
if (restrictToMarks && marks) {
const nextValue = getPreviousMarkValue(_value, marks);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
const nextValue = getFloatingValue(Math.min(Math.max(_value - step!, min!), max!), precision);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
case "ArrowLeft": {
event.preventDefault();
thumb.current?.focus();
if (restrictToMarks && marks) {
const nextValue = dir === "rtl" ? getNextMarkValue(_value, marks) : getPreviousMarkValue(_value, marks);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
const nextValue = getFloatingValue(Math.min(Math.max(dir === "rtl" ? _value + step! : _value - step!, min!), max!), precision);
setValue(nextValue);
onChangeEnd?.(nextValue);
break;
}
case "Home": {
event.preventDefault();
thumb.current?.focus();
if (restrictToMarks && marks) {
setValue(getFirstMarkValue(marks));
onChangeEnd?.(getFirstMarkValue(marks));
break;
}
setValue(min!);
onChangeEnd?.(min!);
break;
}
case "End": {
event.preventDefault();
thumb.current?.focus();
if (restrictToMarks && marks) {
setValue(getLastMarkValue(marks));
onChangeEnd?.(getLastMarkValue(marks));
break;
}
setValue(max!);
onChangeEnd?.(max!);
break;
}
default: {
break;
}
}
}
};
const stylesApi = { unstyled, classNames, styles, disabled };
return (
<ctx.Provider value={{ dir, getStyles }}>
<Edge
key={dir}
{...{
el: "div",
selector: "root",
ref: useMergedRef(ref, root),
onKeyDownCapture: handleTrackKeydownCapture,
onMouseDownCapture: () => root.current?.focus(),
dir,
size,
round,
...stylesApi,
...props
}}
>
<SliderTrack
{...{
inverted,
offset: 0,
position,
marks,
min,
max,
value: scaledValue,
containerProps: {
ref: container as any,
onMouseEnter: showLabelOnHover ? () => setHovered(true) : undefined,
onMouseLeave: showLabelOnHover ? () => setHovered(false) : undefined
},
...stylesApi
}}
>
<SliderThumb
{...{
max,
min,
value: scaledValue,
position,
dragging: active,
label: _label,
ref: thumb as any,
labelTransitionProps,
labelAlwaysOn,
thumbLabel,
showLabelOnHover,
isHovered: hovered,
...stylesApi,
...thumbProps
}}
>
{thumbChildren}
</SliderThumb>
</SliderTrack>
<input type="hidden" id={name} name={name} value={scaledValue} {...hiddenInputProps} />
</Edge>
</ctx.Provider>
);
}) as SliderComponent;
Slider.displayName = "Slider";
export type RangeSliderValue = [number, number];
export interface RangeSliderProps extends ComponentProps<"div", "onChange" | "value" | "defaultValue"> {
color?: CSSProperties["color"];
round?: (string & {}) | number;
size?: (string & {}) | number;
min?: number;
max?: number;
step?: number;
precision?: number;
value?: RangeSliderValue;
defaultValue?: RangeSliderValue;
onChange?: (value: RangeSliderValue) => void;
onChangeEnd?: (value: RangeSliderValue) => void;
name?: string;
marks?: { value: number; label?: React.ReactNode }[];
label?: React.ReactNode | ((value: number) => React.ReactNode);
labelTransitionProps?: TransitionOverride;
labelAlwaysOn?: boolean;
showLabelOnHover?: boolean;
thumbChildren?: React.ReactNode;
disabled?: boolean;
thumbSize?: number | string;
scale?: (value: number) => number;
inverted?: boolean;
minRange?: number;
maxRange?: number;
thumbFromLabel?: string;
thumbToLabel?: string;
hiddenInputProps?: React.ComponentPropsWithoutRef<"input">;
thumbProps?: (index: 0 | 1) => React.ComponentPropsWithoutRef<"div">;
dir?: "ltr" | "rtl";
}
export const RangeSlider = React.forwardRef<HTMLDivElement, RangeSliderProps>((_props, ref) => {
const {
classNames,
styles,
value,
onChange,
onChangeEnd,
size,
maxRange,
precision: _precision,
defaultValue,
name,
thumbFromLabel,
thumbToLabel,
thumbChildren,
unstyled,
inverted,
dir = "ltr",
hiddenInputProps,
thumbProps,
min = 0,
max = 100,
minRange = 10,
step = 1,
marks = [],
label = f => f,
labelTransitionProps = { transition: "fade", duration: 0 },
labelAlwaysOn = false,
showLabelOnHover = true,
disabled = false,
scale = v => v,
round,
...props
} = _props;
const [focused, setFocused] = React.useState(-1);
const [hovered, setHovered] = React.useState(false);
const [_value, setValue] = useUncontrolled<RangeSliderValue>({
value,
defaultValue,
finalValue: [min!, max!],
onChange
});
const valueRef = React.useRef(_value);
const thumbs = React.useRef<HTMLDivElement[]>([]);
const thumbIndex = React.useRef<number | undefined>(undefined);
const positions = [getPosition({ value: _value[0], min: min!, max: max! }), getPosition({ value: _value[1], min: min!, max: max! })];
const precision = _precision ?? getPrecision(step!);
const _setValue = (val: RangeSliderValue) => {
setValue(val);
valueRef.current = val;
};
React.useEffect(
() => {
if (Array.isArray(value)) {
valueRef.current = value;
}
},
Array.isArray(value) ? [value[0], value[1]] : [null, null]
);
const setRangedValue = (val: number, index: number, triggerChangeEnd: boolean) => {
const clone: RangeSliderValue = [...valueRef.current];
clone[index] = val;
if (index === 0) {
if (val > clone[1] - (minRange! - 0.000000001)) {
clone[1] = Math.min(val + minRange!, max!);
}
if (val > (max! - (minRange! - 0.000000001) || min!)) {
clone[index] = valueRef.current[index];
}
if (clone[1] - val > maxRange!) {
clone[1] = val + maxRange!;
}
}
if (index === 1) {
if (val < clone[0] + minRange!) {
clone[0] = Math.max(val - minRange!, min!);
}
if (val < clone[0] + minRange!) {
clone[index] = valueRef.current[index];
}
if (val - clone[0] > maxRange!) {
clone[0] = val - maxRange!;
}
}
clone[0] = getFloatingValue(clone[0], precision);
clone[1] = getFloatingValue(clone[1], precision);
_setValue(clone);
if (triggerChangeEnd) {
onChangeEnd?.(valueRef.current);
}
};
const handleChange = (val: number) => {
if (!disabled) {
const nextValue = getChangeValue({
value: val,
min: min!,
max: max!,
step: step!,
precision
});
setRangedValue(nextValue, thumbIndex.current!, false);
}
};
const { ref: container, active } = useMove(({ x }) => handleChange(x), { onScrubEnd: () => onChangeEnd?.(valueRef.current) }, dir);
function handleThumbMouseDown(index: number) {
thumbIndex.current = index;
}
const handleTrackMouseDownCapture = (event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
container.current!.focus();
const rect = container.current!.getBoundingClientRect();
const changePosition = getClientPosition(event.nativeEvent);
const changeValue = getChangeValue({
value: changePosition - rect.left,
max: max!,
min: min!,
step: step!,
containerWidth: rect.width
});
const nearestHandle = Math.abs(_value[0] - changeValue) > Math.abs(_value[1] - changeValue) ? 1 : 0;
const _nearestHandle = dir === "ltr" ? nearestHandle : nearestHandle === 1 ? 0 : 1;
thumbIndex.current = _nearestHandle;
};
const getFocusedThumbIndex = () => {
if (focused !== 1 && focused !== 0) {
setFocused(0);
return 0;
}
return focused;
};
const handleTrackKeydownCapture = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!disabled) {
switch (event.key) {
case "ArrowUp": {
event.preventDefault();
const focusedIndex = getFocusedThumbIndex();
thumbs.current[focusedIndex].focus();
setRangedValue(getFloatingValue(Math.min(Math.max(valueRef.current[focusedIndex] + step!, min!), max!), precision), focusedIndex, true);
break;
}
case "ArrowRight": {
event.preventDefault();
const focusedIndex = getFocusedThumbIndex();
thumbs.current[focusedIndex].focus();
setRangedValue(
getFloatingValue(
Math.min(Math.max(dir === "rtl" ? valueRef.current[focusedIndex] - step! : valueRef.current[focusedIndex] + step!, min!), max!),
precision
),
focusedIndex,
true
);
break;
}
case "ArrowDown": {
event.preventDefault();
const focusedIndex = getFocusedThumbIndex();
thumbs.current[focusedIndex].focus();
setRangedValue(getFloatingValue(Math.min(Math.max(valueRef.current[focusedIndex] - step!, min!), max!), precision), focusedIndex, true);
break;
}
case "ArrowLeft": {
event.preventDefault();
const focusedIndex = getFocusedThumbIndex();
thumbs.current[focusedIndex].focus();
setRangedValue(
getFloatingValue(
Math.min(Math.max(dir === "rtl" ? valueRef.current[focusedIndex] + step! : valueRef.current[focusedIndex] - step!, min!), max!),
precision
),
focusedIndex,
true
);
break;
}
default: {
break;
}
}
}
};
const sharedThumbProps = {
max: max!,
min: min!,
size,
labelTransitionProps,
labelAlwaysOn,
onBlur: () => setFocused(-1)
};
const hasArrayThumbChildren = Array.isArray(thumbChildren);
const stylesApi = { unstyled, classNames, styles, disabled };
return (
<ctx.Provider value={{ dir, getStyles }}>
<Edge key={dir} {...{ el: "div", selector: "root", ref, dir, size, round, ...stylesApi, ...props }}>
<SliderTrack
{...{
offset: positions[0],
marksOffset: _value[0],
position: positions[1] - positions[0],
marks,
inverted,
min,
max,
value: _value[1],
containerProps: {
ref: container as any,
onMouseEnter: showLabelOnHover ? () => setHovered(true) : undefined,
onMouseLeave: showLabelOnHover ? () => setHovered(false) : undefined,
onTouchStartCapture: handleTrackMouseDownCapture,
onTouchEndCapture: () => {
thumbIndex.current = -1;
},
onMouseDownCapture: handleTrackMouseDownCapture,
onMouseUpCapture: () => {
thumbIndex.current = -1;
},
onKeyDownCapture: handleTrackKeydownCapture
},
...stylesApi
}}
>
<SliderThumb
{...{
value: scale!(_value[0]),
position: positions[0],
dragging: active,
label: typeof label === "function" ? label(getFloatingValue(scale!(_value[0]), precision)) : label,
ref: node => {
thumbs.current[0] = node!;
},
thumbLabel: thumbFromLabel,
onMouseDown: () => handleThumbMouseDown(0),
onFocus: () => setFocused(0),
showLabelOnHover,
isHovered: hovered,
...thumbProps?.(0),
...sharedThumbProps,
...stylesApi,
...thumbProps
}}
>
{hasArrayThumbChildren ? thumbChildren[0] : thumbChildren}
</SliderThumb>
<SliderThumb
{...{
thumbLabel: thumbToLabel,
value: scale!(_value[1]),
position: positions[1],
dragging: active,
label: typeof label === "function" ? label(getFloatingValue(scale!(_value[1]), precision)) : label,
ref: node => {
thumbs.current[1] = node!;
},
onMouseDown: () => handleThumbMouseDown(1),
onFocus: () => setFocused(1),
showLabelOnHover,
isHovered: hovered,
...thumbProps?.(1),
...sharedThumbProps,
...stylesApi,
...thumbProps
}}
>
{hasArrayThumbChildren ? thumbChildren[1] : thumbChildren}
</SliderThumb>
</SliderTrack>
<input type="hidden" name={`${name}_from`} value={_value[0]} {...hiddenInputProps} />
<input type="hidden" name={`${name}_to`} value={_value[1]} {...hiddenInputProps} />
</Edge>
</ctx.Provider>
);
});
RangeSlider.displayName = "RangeSlider";
export interface TrackProps extends ComponentProps<"div"> {
position: number;
offset?: number;
marksOffset?: number;
marks: { value: number; label?: React.ReactNode }[] | undefined;
min: number;
max: number;
value: number;
children: React.ReactNode;
containerProps?: React.PropsWithRef<React.ComponentProps<"div">>;
disabled: boolean | undefined;
inverted: boolean | undefined;
}
export const SliderTrack = React.forwardRef<HTMLDivElement, TrackProps>((_props, ref) => {
const { unstyled, classNames, styles, position, children, offset, disabled, marksOffset, inverted, containerProps, ...props } = _props;
const stylesRest = { unstyled, classNames, styles, disabled };
const stylesApi = { ...stylesRest, inverted };
return (
<Edge
{...{
el: "div",
selector: "trackContainer",
ref,
...stylesRest,
...containerProps
}}
>
<Edge {...{ el: "div", selector: "track", ...stylesApi }}>
<Edge
{...{
el: "div",
selector: "bar",
style: {
"--slider-bar-width": `calc(${position}% + var(--slider-size))`,
"--slider-bar-offset": `calc(${offset}% - var(--slider-size))`
},
...stylesApi
}}
/>
{children}
<SliderMarks {...{ ...props, offset: marksOffset, ...stylesApi }} />
</Edge>
</Edge>
);
});
SliderTrack.displayName = "Slider/SliderTrack";
export interface SliderMarksProps extends ComponentProps<"div"> {
marks: { value: number; label?: React.ReactNode }[] | undefined;
min: number;
max: number;
value: number;
offset: number | undefined;
disabled: boolean | undefined;
inverted: boolean | undefined;
}
export const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>((_props, ref) => {
const { unstyled, classNames, styles, marks, min, max, disabled, value, offset, inverted, ...props } = _props;
const stylesRest = { unstyled, classNames, styles, disabled };
if (!marks) return null;
const items = marks.map((mark, index) => (
<Edge
key={index}
{...{
el: "div",
selector: "markWrapper",
ref,
style: {
"--mark-offset": `${getPosition({ value: mark.value, min, max })}%`
},
...stylesRest,
...props
}}
>
<Edge
{...{
el: "div",
selector: "mark",
"data-filled": isMarkFilled({ mark, value, offset, inverted }) ? "true" : undefined,
...stylesRest
}}
/>
{mark.label && <Edge {...{ el: "div", selector: "markLabel", ...stylesRest }}>{mark.label}</Edge>}
</Edge>
));
return items.length ? <div>{items}</div> : null;
});
SliderMarks.displayName = "Slider/SliderMarks";
export interface ThumbProps extends ComponentProps<"div", "value"> {
max: number;
min: number;
value: number;
position: number;
dragging: boolean;
label: React.ReactNode;
onKeyDownCapture?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
onMouseDown?: (event: any) => void;
labelTransitionProps: TransitionOverride | undefined;
labelAlwaysOn: boolean | undefined;
thumbLabel: string | undefined;
showLabelOnHover: boolean | undefined;
isHovered?: boolean;
children?: React.ReactNode;
disabled: boolean | undefined;
className?: string;
}
export const SliderThumb = React.forwardRef<HTMLDivElement, ThumbProps>((_props, ref) => {
const {
unstyled,
classNames,
styles,
style,
max,
min,
value,
position,
label,
dragging,
onMouseDown,
onKeyDownCapture,
labelTransitionProps,
labelAlwaysOn,
thumbLabel,
onFocus,
onBlur,
isHovered,
showLabelOnHover,
children = null,
disabled,
role = "slider",
...props
} = _props;
const [focused, setFocused] = React.useState(false);
const stylesApi = { unstyled, classNames, styles };
const isVisible = labelAlwaysOn || dragging || focused || (showLabelOnHover && isHovered);
const handleFocus = (event: React.FocusEvent<HTMLDivElement, Element>) => {
setFocused(true);
typeof onFocus === "function" && onFocus(event);
};
const handleBlur = (event: React.FocusEvent<HTMLDivElement, Element>) => {
setFocused(false);
typeof onBlur === "function" && onBlur(event);
};
const handleClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.stopPropagation();
};
return (
<Edge
{...{
el: "div",
selector: "thumb",
ref,
role,
disabled,
tabIndex: 0,
"aria-label": thumbLabel,
"aria-valuemax": max,
"aria-valuemin": min,
"aria-valuenow": value,
"data-dragging": dragging ? "true" : undefined,
onTouchStart: onMouseDown,
onMouseDown: onMouseDown,
onKeyDownCapture: onKeyDownCapture,
onFocus: handleFocus,
onBlur: handleBlur,
onClick: handleClick,
style: { "--slider-thumb-offset": `${position}%`, ...style },
...stylesApi,
...props
}}
>
{children}
<Transition duration={0} transition="fade" mounted={label != null && !!isVisible} {...labelTransitionProps}>
{transitionStyles => <Edge {...{ el: "div", selector: "label", style: transitionStyles, ...stylesApi }}>{label}</Edge>}
</Transition>
</Edge>
);
});
SliderThumb.displayName = "Slider/SliderThumb";
type EdgeType<T extends React.ElementType, Exclude extends string = never> = ComponentProps<T> &
__SliderProps &
Omit<
{
selector?: __Selector;
el?: T;
dir?: string;
ref?: React.ComponentPropsWithRef<T>["ref"];
},
Exclude
>;
const Edge = React.forwardRef(function Edge<T extends React.ElementType>(_props: EdgeType<T, "ref">, ref: React.ComponentPropsWithRef<T>["ref"]) {
const { unstyled, className, classNames, el, style, styles, selector, size, thumbSize, round, disabled, color, inverted, dir, ...props } = _props;
const ctx = useSlider();
const Components = (el || "div") as React.ElementType;
return (
<Components
{...{
ref,
dir: ctx?.dir ?? dir,
...ctx.getStyles(selector as __Selector, { unstyled, className, classNames, style, styles, size, thumbSize, color, inverted, disabled, round, ...ctx }),
...props
}}
/>
);
}) as <T extends React.ElementType>(_props: EdgeType<T>) => React.ReactElement;
interface GetChangeValue {
value: number;
containerWidth?: number;
min: number;
max: number;
step: number;
precision?: number;
}
export function getChangeValue({ value, containerWidth, min, max, step, precision }: GetChangeValue) {
const left = !containerWidth ? value : Math.min(Math.max(value, 0), containerWidth) / containerWidth;
const dx = left * (max - min);
const nextValue = (dx !== 0 ? Math.round(dx / step) * step : 0) + min;
const nextValueWithinStep = Math.max(nextValue, min);
if (precision !== undefined) return Number(nextValueWithinStep.toFixed(precision));
return nextValueWithinStep;
}
export function getPrecision(step: number) {
if (!step) return 0;
const split = step.toString().split(".");
return split.length > 1 ? split[1].length : 0;
}
interface GetPosition {
value: number;
min: number;
max: number;
}
export function getPosition({ value, min, max }: GetPosition) {
const position = ((value - min) / (max - min)) * 100;
return Math.min(Math.max(position, 0), 100);
}
export function findClosestNumber(value: number, numbers: number[]): number {
if (numbers.length === 0) return value;
return numbers.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));
}
export function getNextMarkValue(currentValue: number, marks: { value: number; label?: React.ReactNode }[]) {
const sortedMarks = [...marks].sort((a, b) => a.value - b.value);
const nextMark = sortedMarks.find(mark => mark.value > currentValue);
return nextMark ? nextMark.value : currentValue;
}
export function getPreviousMarkValue(currentValue: number, marks: { value: number; label?: React.ReactNode }[]) {
const sortedMarks = [...marks].sort((a, b) => b.value - a.value);
const previousMark = sortedMarks.find(mark => mark.value < currentValue);
return previousMark ? previousMark.value : currentValue;
}
export function getFirstMarkValue(marks: { value: number; label?: React.ReactNode }[]) {
const sortedMarks = [...marks].sort((a, b) => b.value - a.value);
return sortedMarks.length > 0 ? sortedMarks[0].value : 0;
}
export function getLastMarkValue(marks: { value: number; label?: React.ReactNode }[]) {
const sortedMarks = [...marks].sort((a, b) => a.value - b.value);
return sortedMarks.length > 0 ? sortedMarks[sortedMarks.length - 1].value : 100;
}
export function getFloatingValue(value: number, precision: number) {
return parseFloat(value.toFixed(precision));
}
export function getClientPosition(event: any) {
if ("TouchEvent" in window && event instanceof window.TouchEvent) {
const touch = event.touches[0];
return touch.clientX;
}
return event.clientX;
}
interface IsMarkFilled {
mark: { value: number; label?: any };
offset?: number;
value: number;
inverted?: boolean;
}
export function isMarkFilled({ mark, offset, value, inverted = false }: IsMarkFilled) {
return inverted
? typeof offset === "number"
? mark.value <= offset || mark.value >= value
: mark.value >= value
: typeof offset === "number"
? mark.value >= offset && mark.value <= value
: mark.value <= value;
}
// Export as a composite component
type SliderComponent = React.ForwardRefExoticComponent<SliderProps> & {
Track: typeof SliderTrack;
Thumb: typeof SliderThumb;
Marks: typeof SliderMarks;
};
// Attach sub-components
Slider.Track = SliderTrack;
Slider.Thumb = SliderThumb;
Slider.Marks = SliderMarks;
globals.css
/* slider */
@layer base {
.stylelayer-slider {
@apply w-full h-[calc(var(--slider-size)*2)] px-[--slider-size] flex flex-col items-center touch-none relative outline-0 [-webkit-tap-highlight-color:transparent] [--spacing:0.625rem];
& .slider-label {
@apply absolute top-[calc((var(--slider-label-fz)*(250/100))*-1)] rounded-[.25rem] py-0.5 px-1 [font-size:--slider-label-fz] whitespace-nowrap pointer-events-none select-none touch-none bg-muted text-color;
}
& .slider-thumb {
@apply absolute flex items-center justify-center z-[4] select-none touch-none size-[--slider-thumb-size] [border:.25rem_solid] [transform:translate(-50%,-50%)] top-1/2 cursor-pointer rounded-[--slider-round] outline-offset-2 left-[--slider-thumb-left,var(--slider-thumb-offset)] aria-disabled:hidden data-[disabled]:hidden data-[dragging]:[transform:translate(-50%,-50%)_scale(1.05)] data-[dragging]:shadow-md border-constructive text-color bg-[--slider-color,hsl(var(--muted))];
&:where([dir="rtl"]) {
@apply [--slider-thumb-left:auto] right-[calc(var(--slider-thumb-offset)-var(--slider-thumb-size))];
}
}
& .slider-track-container {
@apply flex items-center w-full h-[calc(var(--slider-size)*2)] cursor-pointer data-[disabled]:cursor-not-allowed aria-disabled:cursor-not-allowed;
}
& .slider-track {
@apply relative w-full h-[--slider-size] flex items-center justify-center;
&:where([data-inverted]:not([data-disabled])) {
@apply [--track-bg:--slider-color];
}
fieldset:disabled &:where([data-inverted]),
&:where([data-inverted][data-disabled]) {
@apply [--track-bg:--slider-track-disabled-bg];
}
&::before {
@apply content-[''] absolute inset-y-0 inset-x-[calc(var(--slider-size)*-1)] z-0 rounded-[--slider-round] bg-[--track-bg,var(--slider-track-bg)];
}
}
& .slider-bar {
@apply absolute z-1 inset-y-0 bg-[--slider-color] rounded-[--slider-round] w-[--slider-bar-width] left-[--slider-bar-left,var(--slider-bar-offset)];
&:where([dir="rtl"]) {
@apply [--slider-bar-left:auto] right-[--slider-bar-offset];
}
&:where([data-inverted]) {
@apply bg-[--slider-track-bg];
}
fieldset:disabled &:where(:not([data-inverted])),
&:where([data-disabled]:not([data-inverted])) {
@apply bg-muted;
}
}
& .slider-mark-wrapper {
@apply absolute flex items-center justify-center z-2 h-0 pointer-events-none left-[--slider-mark-wrapper-left,var(--slider-mark-wrapper-position)] [--slider-mark-wrapper-position:calc(var(--mark-offset)-var(--slider-size)/2)];
&:where([dir="rtl"]) {
@apply [--slider-mark-wrapper-left:auto] right-[--slider-mark-wrapper-position];
}
}
& .slider-mark {
@apply [border:.125rem_solid_var(--slider-mark-border,var(--track-bg,var(--slider-track-bg)))] size-[--slider-size] rounded-full bg-white pointer-events-none;
&:where([data-filled]) {
@apply [--slider-mark-border:--slider-color];
&:where([data-disabled]) {
@apply border-muted;
}
}
}
& .slider-mark-label {
@apply absolute [font-size:--slider-label-fz] top-[var(--mark-label-y,calc((var(--slider-thumb-size)/2)+(var(--spacing)/2)))] whitespace-nowrap cursor-pointer select-none text-muted-foreground;
&:where([dir="rtl"]) {
@apply [--slider-mark-label-x:50%] [--transform:translate(calc(var(--slider-mark-label-x,-50%)+var(--slider-size)/2),calc(var(--spacing)/2))];
}
}
}
}