Getting Started
App Providerglobal.d.tsThemesTypes
cn(...args)Text ParserUnits Converters
AnchorAvatarBreadcrumbBurgerButtonCardCarouselCheckerCodeColor PickerCommandConfettiCopyButtonDouble Helix WordsFloating IndicatorGroupHighlight TextIndicatorInputKbdLabelLoaderPaginationPassword RequirementPolymorphic SlotProgressProseRatingRunning AreaScroll AreaSheetsSkeletonSliderStackSvgTableTabsTextareaTimelineTimesToasterTooltipTyping WordsTypography
useClickOutsideuseClipboarduseDeviceInfouseDialoguseDidUpdateuseDirectionuseDisclosureuseDocumentTitleuseDocumentVisibilityuseElementInfouseEyeDropperuseFetchuseFullscreenuseGeoLocationuseHotkeysuseHoveruseIduseImagePopupuseInputStateuseIntersectionuseIntervaluseIsomorphicEffectuseListStateuseLocalStorageuseMeasureScrollbaruseMediaQueryuseMergedRefuseMouseuseMoveuseMutationObserveruseNetworkuseOpenStateuseOrientationuseOSusePaginationusePWAInstalleruseRandomColorsuseReducedMotionuseReloaduseResizeObserveruseScrollIntoViewuseStateHistoryuseTimeoutuseTouchuseTriggeruseUncontrolleduseValidatedStateuseViewportSizeuseWindowEventuseWindowScroll
Docs
Web
Components
Slider
Skeleton

A placeholder UI used to indicate loading states by imitating content structure.

Stack

A vertical stack layout component for cleanly aligning elements with spacing.


Edit this page on GitHub
  • Started
  • Utilities
  • Configuration
  • Components
  • Hooks
  • Examples
  • Github
  • Contributing
⌘+J

© 2025 oeri rights MIT


Designed in Earth-616

Built by oeri

Slider

An interactive input component that lets users pick a numeric value or range by dragging a handle along a track.


The Slider component allows users to select a single value or a range of values along a track. It is highly customizable, supporting features like marks, custom labels, scaling functions, inverted behavior, and adjustable step sizes. Ideal for forms, settings adjustments, or any interface where controlled numeric input is needed with a more intuitive and interactive approach.


Usage

Basic usage example to quickly see how the Slider works and how it handles basic value selection across a specified range.

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

Interactive configurator to explore the customization options for the Slider component, including its behavior, appearance, and interaction states.

25%
50%
75%
onChange value: 35onChangeEnd value: 35
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

Demonstrates how to invert the value progression of the Slider, making it decrease instead of increase as the thumb moves forward.

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

Showcases how to display custom labels on the Slider to provide users with more informative value hints or steps.

No label
Formatted label
Label always visible
40
Custom label transition
"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

Illustrates how to add predefined marks or points along the track to guide user selection on the Slider.

Marks
xs
sm
md
lg
xl
20%
50%
80%
Restrict selection to marks
Disabled
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

Demonstrates applying a custom scaling function to map slider values differently (e.g., logarithmic scales for non-linear data representation).

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

Shows how to configure the Slider to move in specific step increments rather than continuously, improving precision for discrete selections.

Decimal Values

Decimal step
Step 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

Explores customization options for the Slider thumb, including styling and behavior adjustments for better user experience.

Thumb Icon
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 APITypeDefaultAnnotation
unstyled?Partial<Record<T, boolean>>falseif true, default styles will be removed
className?stringundefinedpass to root component <div>
classNames?Partial<Record<T, string>>undefined
style?CSSPropertiesundefinedpass to root component <div>
styles?Partial<Record<T, CSSProperties>>undefined

Props API

Props APITypeDefaultAnnotation
dir?"ltr" | "rtl"ltrType of direction slider
color?CSSProperties["color"]hsl(var(--constructive))Key of valid CSS color, controls color of track and thumb
round?number | string999Key of valid CSS value to set border-radius, numbers are converted to rem
size?(string & {}) | number12Controls size of the track
min?number0Minimal possible value
max?number100Maximum possible value
step?number1Number by which value will be incremented/decremented with thumb drag and arrows
precision?numberNumber of significant digits after the decimal point
value?numberControlled component value
defaultValue?numberUncontrolled component default value
onChange?(value: number) => voidCalled when value changes
onChangeEnd?(value: number) => voidCalled when user stops dragging slider or changes value with arrows
name?stringHidden 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?TransitionOverride0Props passed down to the Transition component, { transition: 'fade', duration: 0 }
labelAlwaysOn?booleanfalseDetermines whether the label should be visible when the slider is not being dragged or hovered
thumbLabel?stringThumb aria-label
showLabelOnHover?booleantrueDetermines whether the label should be displayed when the slider is hovered
thumbChildren?React.ReactNodeContent rendered inside thumb
disabled?booleanfalseDisables slider
thumbSize?number | stringsizeThumb width and height, by default value is computed based on size prop
scale?(value: number) => numberA transformation function to change the scale of the slider
inverted?booleanfalseDetermines whether track value representation should be inverted
hiddenInputProps?React.ComponentPropsWithoutRef<"input">Props passed down to the hidden input
restrictToMarks?booleanfalseDetermines whether the selection should be only allowed from the given marks array
thumbProps?React.ComponentPropsWithoutRef<"div">Props passed down to thumb element

Source Codes

Full working code example, including necessary markup and styles. You can copy and paste this code directly to start using the component immediately.

slider.tsx
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))]; } } } }