dotUI
dotUI
beta
  1. Components
  2. Inputs
  3. Slider

Slider

An input where the user selects a value from within a given range.

<Slider defaultValue={50} />

Installation

Install the following dependencies:

npm install react-aria-components @react-aria/utils

Copy and paste the following code into your project.

"use client";

import * as React from "react";
import { useSlotId } from "@react-aria/utils";
import {
  composeRenderProps,
  Slider as AriaSlider,
  SliderOutput as AriaSliderOutput,
  SliderThumb as AriaSliderThumb,
  SliderTrack as AriaSliderTrack,
  type SliderThumbProps as AriaSliderThumbProps,
  type SliderOutputProps as AriaSliderOutputProps,
  type SliderTrackProps as AriaSliderTrackProps,
  type SliderProps as AriaSliderProps,
  SliderStateContext as AriaSliderStateContext,
  TextContext as AriaTextContext,
} from "react-aria-components";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils/classes";
import { focusRing } from "@/lib/utils/styles";
import { Description, Label, type FieldProps } from "./field";

const sliderStyles = tv({
  slots: {
    root: "flex flex-col gap-2 orientation-horizontal:w-48 orientation-vertical:h-48 orientation-vertical:items-center",
    track: [
      "relative group/track rounded-full bg-bg-neutral cursor-pointer disabled:cursor-default disabled:bg-bg-disabled",
      "grow orientation-vertical:flex-1 orientation-vertical:w-2 orientation-horizontal:w-full orientation-horizontal:h-2",
    ],
    filler: [
      "rounded-full bg-border-focus group-disabled/track:bg-bg-disabled",
      "pointer-events-none absolute group-orientation-horizontal/top-0 group-orientation-vertical/track:w-full group-orientation-vertical/track:bottom-0 group-orientation-horizontal/track:h-full",
    ],
    thumb: [
      focusRing(),
      "rounded-full bg-white shadow-md transition-[width,height]",
      "absolute left-[50%] top-[50%] block !-translate-x-1/2 !-translate-y-1/2",
      "disabled:bg-bg-disabled disabled:border disabled:border-bg",
    ],
    valueLabel: "text-fg-muted text-sm",
  },
  variants: {
    size: {
      sm: {
        thumb: "size-3 dragging:size-4",
        track: "orientation-vertical:w-1 orientation-horizontal:h-1",
      },
      md: {
        thumb: "size-4 dragging:size-5",
        track: "orientation-vertical:w-2 orientation-horizontal:h-2",
      },
      lg: {
        thumb: "size-5 dragging:size-6",
        track: "orientation-vertical:w-3 orientation-horizontal:h-3",
      },
    },
  },
  defaultVariants: {
    size: "md",
  },
});

interface SliderProps extends SliderRootProps, VariantProps<typeof sliderStyles> {
  label?: FieldProps["label"];
  description?: FieldProps["description"];
  valueLabel?: boolean | ((value: number[]) => string);
}
const Slider = React.forwardRef<React.ElementRef<typeof AriaSlider>, SliderProps>(
  ({ label, description, valueLabel = false, size, ...props }, ref) => (
    <SliderRoot ref={ref} {...props}>
      {(label || !!valueLabel) && (
        <div className={cn("flex items-center justify-between gap-2", !label && "justify-end")}>
          {label && <Label>{label}</Label>}
          {!!valueLabel && (
            <SliderValueLabel>
              {({ state }) =>
                typeof valueLabel === "function" ? valueLabel(state.values) : undefined
              }
            </SliderValueLabel>
          )}
        </div>
      )}
      <SliderControls size={size} />
      {description && <Description>{description}</Description>}
    </SliderRoot>
  )
);
Slider.displayName = "Slider";

type SliderRootProps = AriaSliderProps;
const SliderRoot = React.forwardRef((props: SliderRootProps, ref: React.Ref<HTMLDivElement>) => {
  const { root } = sliderStyles();
  const descriptionId = useSlotId();
  return (
    <AriaTextContext.Provider value={{ slots: { description: { id: descriptionId } } }}>
      <AriaSlider
        ref={ref}
        aria-describedby={descriptionId}
        {...props}
        className={composeRenderProps(props.className, (className) => root({ className }))}
      />
    </AriaTextContext.Provider>
  );
});
SliderRoot.displayName = "SliderRoot";

type SliderControlsProps = SliderTrackProps & VariantProps<typeof sliderStyles>;
const SliderControls = (props: SliderControlsProps) => {
  const { values } = React.useContext(AriaSliderStateContext);
  return (
    <SliderTrack {...props}>
      <SliderFiller />
      {values.map((_, i) => (
        <SliderThumb key={i} index={i} size={props.size} />
      ))}
    </SliderTrack>
  );
};

type SliderTrackProps = AriaSliderTrackProps & VariantProps<typeof sliderStyles>;
const SliderTrack = ({ size, ...props }: SliderTrackProps) => {
  const { track } = sliderStyles({ size });
  return (
    <AriaSliderTrack
      {...props}
      className={composeRenderProps(props.className, (className) => track({ className }))}
    />
  );
};

type SliderFillerProps = React.HTMLAttributes<HTMLDivElement>;
const SliderFiller = (props: SliderFillerProps) => {
  const { filler } = sliderStyles();
  const { orientation, getThumbPercent, values } = React.useContext(AriaSliderStateContext);
  return (
    <div
      {...props}
      style={
        values.length === 1
          ? orientation === "horizontal"
            ? {
                width: `${getThumbPercent(0) * 100}%`,
              }
            : { height: `${getThumbPercent(0) * 100}%` }
          : orientation === "horizontal"
            ? {
                left: `${getThumbPercent(0) * 100}%`,
                width: `${Math.abs(getThumbPercent(0) - getThumbPercent(1)) * 100}%`,
              }
            : {
                bottom: `${getThumbPercent(0) * 100}%`,
                height: `${Math.abs(getThumbPercent(0) - getThumbPercent(1)) * 100}%`,
              }
      }
      className={filler({ className: props.className })}
    />
  );
};

type SliderThumbProps = AriaSliderThumbProps & VariantProps<typeof sliderStyles>;
const SliderThumb = ({ size, ...props }: SliderThumbProps) => {
  const { thumb } = sliderStyles({ size });
  return (
    <AriaSliderThumb
      {...props}
      className={composeRenderProps(props.className, (className) => thumb({ className }))}
    />
  );
};

type SliderValueLabelProps = AriaSliderOutputProps;
const SliderValueLabel = (props: SliderValueLabelProps) => {
  const { valueLabel } = sliderStyles();
  return (
    <AriaSliderOutput
      {...props}
      className={composeRenderProps(props.className, (className) => valueLabel({ className }))}
    >
      {composeRenderProps(
        props.children,
        (children, { state }) =>
          children ?? state.values.map((_, i) => state.getThumbValueLabel(i)).join(" - ")
      )}
    </AriaSliderOutput>
  );
};

export {
  Slider,
  SliderRoot,
  SliderControls,
  SliderTrack,
  SliderFiller,
  SliderThumb,
  SliderValueLabel,
};

Update the import paths to match your project setup.

Options

Size

Use the size prop to control the size of the slider. The default variant is "md".

<Slider size="sm" />
<Slider size="md" />
<Slider size="lg" />

Orientation

Sliders are horizontally oriented by default. The orientation prop can be set to "vertical" to create a vertical slider.

<Slider orientation="vertical" />

Label

A visual label can be provided using the label prop or a hidden label using aria-label prop.

<Slider label="Opacity" defaultValue={50} />
<Slider aria-label="Opacity" defaultValue={50} />

Description

A description can be supplied to the slider using the description prop.

<Slider label="Opacity" description="Adjust the background opacity." />

Value label

The valueLabel boolean prop can be used to display the current value of the slider. The valueLabel prop can also be set to a function that returns a custom value label.

<Slider label="Donuts to buy" valueLabel />
<Slider
  label="Donuts to buy"
  valueLabel={(donuts) => `${donuts[0]} of 100 Donuts`}
/>

Value scale

By default, slider values are precentages between 0 and 100. A different scale can be used by setting the minValue and maxValue props.

<Slider label="Cookies to buy" minValue={1} maxValue={50} />

Step

The step prop can be used to snap the value to certain increments.

<Slider label="Opacity" step={5} />

Format options

Values are formatted as a percentage by default, but this can be modified by using the formatOptions prop to specify a different format. formatOptions is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale.

<Slider label="Price" formatOptions={{ style: "currency", currency: "JPY" }} />

Range

Passing an array to the defaultValue or value props will create a range slider.

<Slider defaultValue={[200, 300]} />

Disabled

Use the isDisabled prop to disable the slider.

<Slider isDisabled />

Composition

If you need to customize things further, you can drop down to the composition level.

<SliderRoot>
  <div>
    <Label>Volume</Label>
    <SliderValueLabel />
  </div>
  <SliderControls />
</SliderRoot>
<SliderRoot>
  <Label>Volume</Label>
  <SliderValueLabel />
  <div>
    <Volume1Icon />
    <SliderTrack>
      <SliderFiller />
      <SliderThumb />
    </SliderTrack>
    <Volume2Icon />
  </div>
</SliderRoot>

Uncontrolled

The defaultValue prop can be used to set the default state.

<Slider defaultValue={20} />

Controlled

Use the value and onChange props to control the value of the input.

const [value, setValue] = React.useState(50);
return <Slider value={value} onChange={(value) => setValue(value as number)} />;

API Reference

PropTypeDefaultDescription
size
"sm" | "md" | "lg"
"md"
The size of the slider.
valueLabel
boolean | ((value: number[]) => string)
false
Weather to display the value label. A function may be provided to customize the value label.
label
ReactNode
The content to display as the label.
description
ReactNode
A description for the field. Provides a hint such as specific requirements for what to choose.
formatOptions
Intl.NumberFormatOptions
-
The display format of the value label.
orientation
'horizontal' | 'vertical'
'horizontal'
The orientation of the Slider.
isDisabled
boolean
-
Whether the whole Slider is disabled.
minValue
number
0
The slider's minimum value.
maxValue
number
100
The slider's maximum value.
step
number
1
The slider's step value.
value
T
-
The current value (controlled).
defaultValue
T
-
The default value (uncontrolled).
name
string
-
The name of the input element, used when submitting an HTML form.
children
ReactNode | (values: SliderRenderProps & {defaultChildren: ReactNode | undefined}) => ReactNode
-
The children of the component. A function may be provided to alter the children based on component state.
className
string | (values: SliderRenderProps & {defaultClassName: string | undefined}) => string
-
The CSS className for the element. A function may be provided to compute the class based on component state.
style
CSSProperties | (values: SliderRenderProps & {defaultStyle: CSSProperties}) => CSSProperties
-
The inline style for the element. A function may be provided to compute the style based on component state.
EventTypeDescription
onChange
(value: T) => void
Handler that is called when the value changes.
onChangeEnd
(value: T) => void
Fired when the slider stops moving, due to being let go.
Data attributeDescription
[data-orientation="horizontal | vertical"]
The orientation of the slider.
[data-disabled]
Whether the slider is disabled.

Accessibility

Keyboard interactions

KeyDescription
Tab
Places focus on the handle. If the handle is already in focus, moves focus to the next handle or next element in the page tab sequence.
Shift+Tab
Places focus on the previous handle or previous element in the page tab sequence.
ArrowUp ArrowDown ArrowLeft ArrowRight
Moves the handle up/down/left/right.

Built by mehdibha. The source code is available on GitHub.