Slider

Slider component

PackageIcon

Usage

20%
50%
80%
Color
Size
Radius
import { Slider } from '@mantine/core';

function Demo() {
  return (
    <Slider
      color="blue"
      defaultValue={40}
      marks={[
        { value: 20, label: '20%' },
        { value: 50, label: '50%' },
        { value: 80, label: '80%' },
      ]}
    />
  );
}

Controlled

import { useState } from 'react';
import { Slider } from '@mantine/core';

function Demo() {
  const [value, setValue] = useState(40);
  return <Slider value={value} onChange={setValue} />;
}

Uncontrolled

Slider can be used in uncontrolled forms the same way as a native input element. Set the name attribute to include the slider value in the FormData object on form submission. To set the initial value in uncontrolled mode, use the defaultValue prop.

Example usage of uncontrolled Slider with FormData:

import { Slider } from '@mantine/core';

function Demo() {
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        console.log('Slider value:', formData.get('volume'));
      }}
    >
      <Slider name="volume" defaultValue={40} min={0} max={100} />
      <button type="submit">Submit</button>
    </form>
  );
}

Disabled

import { Slider } from '@mantine/core';

function Demo() {
  return <Slider defaultValue={60} disabled />;
}

onChangeEnd

onChangeEnd callback is called when the user stops dragging the slider or when the value is changed with the keyboard. You can use it as a debounced callback to avoid too frequent updates.

onChange value: 50

onChangeEnd value: 50

import { useState } from 'react';
import { Slider, Text, Box } from '@mantine/core';

function Demo() {
  const [value, setValue] = useState(50);
  const [endValue, setEndValue] = useState(50);

  return (
    <Box maw={400} mx="auto">
      <Slider value={value} onChange={setValue} onChangeEnd={setEndValue} />

      <Text mt="md" size="sm">
        onChange value: <b>{value}</b>
      </Text>
      <Text mt={5} size="sm">
        onChangeEnd value: <b>{endValue}</b>
      </Text>
    </Box>
  );
}

Control label

To change label behavior and appearance, set the following props:

  • label – formatter function, accepts value as an argument, set null to disable label, defaults to f => f
  • labelAlwaysOn – if true, the label will always be displayed; by default the label is visible only when the user is dragging
  • labelTransitionProps – props passed down to the Transition component, can be used to customize label animation

No label

Formatted label

Label always visible

40

Custom label transition

import { Slider, Text } from '@mantine/core';

function Demo() {
  return (
    <>
      <Text size="sm">No label</Text>
      <Slider defaultValue={40} label={null} />

      <Text size="sm" mt="xl">Formatted label</Text>
      <Slider defaultValue={40} label={(value) => `${value} °C`} />

      <Text size="sm" mt="xl">Label always visible</Text>
      <Slider defaultValue={40} labelAlwaysOn />

      <Text size="sm" mt="xl">Custom label transition</Text>
      <Slider
        defaultValue={40}
        labelTransitionProps={{
          transition: 'skew-down',
          duration: 150,
          timingFunction: 'linear',
        }}
      />
    </>
  );
}

Min, max and step

Decimal step

Step matched with marks

import { Slider, Text } from '@mantine/core';

const marks = [
  { value: 0, label: 'xs' },
  { value: 25, label: 'sm' },
  { value: 50, label: 'md' },
  { value: 75, label: 'lg' },
  { value: 100, label: 'xl' },
];

function Demo() {
  return (
    <>
      <Text>Decimal step</Text>
      <Slider
        defaultValue={0}
        min={-10}
        max={10}
        label={(value) => value.toFixed(1)}
        step={0.1}
        styles={{ markLabel: { display: 'none' } }}
      />

      <Text mt="md">Step matched with marks</Text>
      <Slider
        defaultValue={50}
        label={(val) => marks.find((mark) => mark.value === val)!.label}
        step={25}
        marks={marks}
        styles={{ markLabel: { display: 'none' } }}
      />
    </>
  );
}

Domain

By default, the min and max props define both the visual range (track display) and the selectable range (possible values). The domain prop allows you to independently control the selectable range. This is useful when you want to display a wider track (for context) but restrict the actual selection to a subset.

In the example below, the track displays from 0 to 100 (min/max), but the thumb can only be dragged between 20 and 80 (domain):

min
max
import { Slider } from '@mantine/core';

function Demo() {
  return (
    <Slider
      domain={[0, 100]}
      min={10}
      max={90}
      defaultValue={25}
      marks={[
        { value: 10, label: 'min' },
        { value: 90, label: 'max' },
      ]}
    />
  );
}

Decimal values

To use Slider with decimal values, set min, max and step props:

import { Slider } from '@mantine/core';

function Demo() {
  return <Slider min={0} max={1} step={0.0005} defaultValue={0.5535} />;
}

Marks

Add any number of marks to the slider by setting the marks prop to an array of objects:

const marks = [
  { value: 20 }, // -> displays mark on slider track
  { value: 40, label: '40%' }, // -> adds mark label below slider track
];

Note that the mark value is relative to the slider value, not width:

20%
50%
80%
import { Slider } from '@mantine/core';

function Demo() {
  return (
    <>
      <Slider defaultValue={40} marks={[{ value: 10 }, { value: 40 }, { value: 95 }]} mb={32} />
      <Slider
        mb={32}
        defaultValue={40}
        marks={[
          { value: 20, label: '20%' },
          { value: 50, label: '50%' },
          { value: 80, label: '80%' },
        ]}
      />
    </>
  );
}

Restrict selection to marks

Set the restrictToMarks prop to restrict the slider value to marks only. Note that in this case the step prop is ignored:

import { RangeSlider, Slider, Stack } from '@mantine/core';

function Demo() {
  return (
    <Stack>
      <Slider
        restrictToMarks
        defaultValue={25}
        marks={Array.from({ length: 5 }).map((_, index) => ({ value: index * 25 }))}
      />

      <RangeSlider
        restrictToMarks
        defaultValue={[5, 15]}
        marks={[
          { value: 5 },
          { value: 15 },
          { value: 25 },
          { value: 35 },
          { value: 70 },
          { value: 80 },
          { value: 90 },
        ]}
      />
    </Stack>
  );
}

Thumb size

Thumb size
import { Slider } from '@mantine/core';

function Demo() {
  return <Slider thumbSize={14} defaultValue={20} />;
}

Thumb children

import { Slider, RangeSlider } from '@mantine/core';
import { HeartIcon, HeartBreakIcon } from '@phosphor-icons/react';

function Demo() {
  return (
    <>
      <Slider
        thumbChildren={<HeartIcon size={16} />}
        color="red"
        label={null}
        defaultValue={40}
        thumbSize={26}
        styles={{ thumb: { borderWidth: 2, padding: 3 } }}
      />

      <RangeSlider
        mt="xl"
        styles={{ thumb: { borderWidth: 2, padding: 3 } }}
        color="red"
        label={null}
        defaultValue={[20, 60]}
        thumbSize={26}
        thumbChildren={[<HeartIcon size={16} key="1" />, <HeartBreakIcon size={16} key="2" />]}
      />
    </>
  );
}

Scale

You can use the scale prop to represent the value on a different scale.

In the following demo, the value x represents the value 2^x. Increasing x by one increases the represented value by 2 to the power of x.

1 MB
1 MB
1 GB
import { RangeSlider, Slider } from '@mantine/core';

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]}`;
}

const getScale = (v: number) => 2 ** v;

function Demo() {
  return (
    <>
      <Slider
        scale={getScale}
        step={1}
        min={2}
        max={30}
        labelAlwaysOn
        defaultValue={10}
        label={valueLabelFormat}
      />
      <RangeSlider
        mt={50}
        scale={getScale}
        step={1}
        min={2}
        max={30}
        labelAlwaysOn
        defaultValue={[10, 20]}
        label={valueLabelFormat}
      />
    </>
  );
}

Inverted

You can invert the track with the inverted prop:

import { Slider } from '@mantine/core';

function Demo() {
  return <Slider inverted defaultValue={80} />;
}

Styles API

Slider supports the Styles API; you can add styles to any inner element of the component with the classNames prop. Follow the Styles API documentation to learn more.

40
20%
80%

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */

Example of using Styles API to change Slider styles:

20%
50%
80%
import { Slider } from '@mantine/core';
import classes from './Demo.module.css';

function Demo() {
  return (
    <Slider
      defaultValue={40}
      size={2}
      classNames={classes}
      marks={[
        { value: 20, label: '20%' },
        { value: 50, label: '50%' },
        { value: 80, label: '80%' },
      ]}
    />
  );
}

Vertical slider

Set orientation="vertical" to render the slider vertically. In vertical orientation, the minimum value is at the bottom and the maximum value is at the top.

20%
50%
80%
20%
50%
80%
import { RangeSlider, Slider } from '@mantine/core';

const marks = [
  { value: 20, label: '20%' },
  { value: 50, label: '50%' },
  { value: 80, label: '80%' },
];

function Demo() {
  return (
    <div style={{ display: 'flex', gap: 40 }}>
      <Slider orientation="vertical" defaultValue={45} marks={marks} />
      <RangeSlider orientation="vertical" defaultValue={[25, 65]} marks={marks} />
    </div>
  );
}

Hidden marks

Hidden marks allow you to snap to specific values without displaying them visually on the track. This is useful when you want to create a "sticky" snapping behavior to certain values that you don't want to show to the user. Use this feature together with restrictToMarks prop:

Hidden marks allow you to snap to specific values without displaying them visually. Current value: 50

0%
50%
100%
import { Slider, Text, Box } from '@mantine/core';
import { useState } from 'react';

function Demo() {
  const [value, setValue] = useState(50);

  return (
    <Box pb="md">
      <Text size="sm" mb="xs">
        Hidden marks allow you to snap to specific values without displaying them visually. Current
        value: {value}
      </Text>
      <Slider
        value={value}
        onChange={setValue}
        min={0}
        max={100}
        step={1}
        restrictToMarks
        marks={[
          { value: 0, label: '0%' },
          { value: 25, hidden: true },
          { value: 50, label: '50%' },
          { value: 75, hidden: true },
          { value: 100, label: '100%' },
        ]}
      />
    </Box>
  );
}

Build custom slider

If the Slider component does not meet your requirements, you can build a custom slider with the use-move hook:

30
70
import { useState } from 'react';
import { DotsSixVerticalIcon } from '@phosphor-icons/react';
import { clamp, useMove } from '@mantine/hooks';
import classes from './Demo.module.css';

function Demo() {
  const [value, setValue] = useState(0.3);
  const { ref } = useMove(({ x }) => setValue(clamp(x, 0.1, 0.9)));
  const labelFloating = value < 0.2 || value > 0.8;

  return (
    <div className={classes.root}>
      <div className={classes.track} ref={ref}>
        <div
          className={classes.filled}
          style={{
            width: `calc(${value * 100}% - var(--thumb-width) / 2 - var(--thumb-offset) / 2)`,
          }}
        >
          <span className={classes.label} data-floating={labelFloating || undefined} data-filled>
            {(value * 100).toFixed(0)}
          </span>
        </div>

        <div
          className={classes.empty}
          style={{
            width: `calc(${(1 - value) * 100}% - var(--thumb-width) / 2 - var(--thumb-offset) / 2)`,
          }}
        >
          <span className={classes.label} data-floating={labelFloating || undefined}>
            {((1 - value) * 100).toFixed(0)}
          </span>
        </div>

        <div
          className={classes.thumb}
          style={{ left: `calc(${value * 100}% - var(--thumb-width) / 2)` }}
        >
          <DotsSixVerticalIcon />
        </div>
      </div>
    </div>
  );
}

Accessibility

Slider component is accessible by default:

  • Thumbs are focusable
  • When the user uses the mouse to interact with the slider, focus is moved to the slider track; when the user presses arrows, focus is moved to the thumb
  • The value can be changed with arrows with step increment/decrement

To label the component for screen readers, add labels to thumbs:

import { Slider } from '@mantine/core';

function Demo() {
  return <Slider thumbLabel="Thumb aria-label" />;
}

Keyboard interactions

KeyDescription
ArrowRight/ArrowUpIncreases the slider value by one step
ArrowLeft/ArrowDownDecreases the slider value by one step
HomeSets the slider value to min value
EndSets the slider value to max value