Skip to contents
This page is an adaptation of the related MUI Material UI documentation page.

Sliders allow users to make selections from a range of values.

Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness, or applying image filters.

Continuous sliders

Continuous sliders allow users to select a value along a subjective range.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';
import VolumeDown from '@mui/icons-material/VolumeDown';
import VolumeUp from '@mui/icons-material/VolumeUp';

export default function ContinuousSlider() {
  const [value, setValue] = React.useState<number>(30);

  const handleChange = (event: Event, newValue: number) => {
    setValue(newValue);
  };

  return (
    <Box sx={{ width: 200 }}>
      <Stack spacing={2} direction="row" sx={{ alignItems: 'center', mb: 1 }}>
        <VolumeDown />
        <Slider aria-label="Volume" value={value} onChange={handleChange} />
        <VolumeUp />
      </Stack>
      <Slider disabled defaultValue={30} aria-label="Disabled slider" />
    </Box>
  );
}
ui <- shinyMaterialUIPage(
  Box(sx = list(width = 200),
    Stack(spacing = 2, direction = "row", sx = list(alignItems = "center", mb = 1),
      shiny::icon("volume-down"),
      Slider.shinyInput(inputId = "volume", value = 30, "aria-label" = "Volume"),
      shiny::icon("volume-up")
    ),
    Slider.shinyInput(inputId = "disabledSlider", value = 30, disabled = TRUE, "aria-label" = "Disabled slider")
  )
)

server <- function(input, output, session) {
  observeEvent(input$volume, {
    # Handle the volume value change
  })
}

shinyApp(ui, server)

Sizes

For smaller slider, use the prop size=“small”.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

export default function SliderSizes() {
  return (
    <Box sx={{ width: 300 }}>
      <Slider
        size="small"
        defaultValue={70}
        aria-label="Small"
        valueLabelDisplay="auto"
      />
      <Slider defaultValue={50} aria-label="Default" valueLabelDisplay="auto" />
    </Box>
  );
}
ui <- CssBaseline(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "smallSlider", value = 70, size = "small", 
                      "aria-label" = "Small", valueLabelDisplay = "auto"),
    Slider.shinyInput(inputId = "defaultSlider", value = 50, 
                      "aria-label" = "Default", valueLabelDisplay = "auto")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Discrete sliders

Discrete sliders can be adjusted to a specific value by referencing its value indicator. You can generate a mark for each step with marks={true}.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

function valuetext(value: number) {
  return `${value}°C`;
}

export default function DiscreteSlider() {
  return (
    <Box sx={{ width: 300 }}>
      <Slider
        aria-label="Temperature"
        defaultValue={30}
        getAriaValueText={valuetext}
        valueLabelDisplay="auto"
        shiftStep={30}
        step={10}
        marks
        min={10}
        max={110}
      />
      <Slider defaultValue={30} step={10} marks min={10} max={110} disabled />
    </Box>
  );
}
ui <- CssBaseline(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "temperatureSlider", value = 30, 
                      "aria-label" = "Temperature", 
                      getAriaValueText = JS("function(value) { return `${value} °C`; }"),
                      valueLabelDisplay = "auto",
                      shiftStep = 30,
                      step = 10,
                      marks = TRUE,
                      min = 10,
                      max = 110),
    Slider.shinyInput(inputId = "disabledMarksSlider", value = 30, 
                      step = 10, marks = TRUE, min = 10, max = 110, disabled = TRUE)
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Small steps

You can change the default step increment. Make sure to adjust the shiftStep prop (the granularity with which the slider can step when using Page Up/Down or Shift + Arrow Up/Down) to a value divisible by the step.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

function valuetext(value: number) {
  return `${value}°C`;
}

export default function DiscreteSliderSteps() {
  return (
    <Box sx={{ width: 300 }}>
      <Slider
        aria-label="Small steps"
        defaultValue={0.00000005}
        getAriaValueText={valuetext}
        step={0.00000001}
        marks
        min={-0.00000005}
        max={0.0000001}
        valueLabelDisplay="auto"
      />
    </Box>
  );
}
ui <- CssBaseline(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "smallStepsSlider", value = 0.00000005, 
                      "aria-label" = "Small steps", 
                      getAriaValueText = JS("function(value) { return `${value} °C`; }"),
                      step = 0.00000001,
                      marks = TRUE,
                      min = -0.00000005,
                      max = 0.0000001,
                      valueLabelDisplay = "auto")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Custom marks

You can have custom marks by providing a rich array to the marks prop.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

const marks = [
  {
    value: 0,
    label: '0°C',
  },
  {
    value: 20,
    label: '20°C',
  },
  {
    value: 37,
    label: '37°C',
  },
  {
    value: 100,
    label: '100°C',
  },
];

function valuetext(value: number) {
  return `${value}°C`;
}

export default function DiscreteSliderMarks() {
  return (
    <Box sx={{ width: 300 }}>
      <Slider
        aria-label="Custom marks"
        defaultValue={20}
        getAriaValueText={valuetext}
        step={10}
        valueLabelDisplay="auto"
        marks={marks}
      />
    </Box>
  );
}
marks <- list(
  list(
    value = 0,
    label = "0°C"
  ),
  list(
    value = 20,
    label = "20°C"
  ),
  list(
    value = 37,
    label = "37°C"
  ),
  list(
    value = 100,
    label = "100°C"
  )
)

ui <- CssBaseline(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "customMarksSlider", value = 20, 
                      "aria-label" = "Custom marks", 
                      getAriaValueText = JS("function(value) { return `${value} °C`; }"),
                      step = 10,
                      valueLabelDisplay = "auto",
                      marks = marks)
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Restricted values

You can restrict the selectable values to those provided with the marks prop with step={null}.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

const marks = [
  {
    value: 0,
    label: '0°C',
  },
  {
    value: 20,
    label: '20°C',
  },
  {
    value: 37,
    label: '37°C',
  },
  {
    value: 100,
    label: '100°C',
  },
];

function valuetext(value: number) {
  return `${value}°C`;
}

export default function DiscreteSliderValues() {
  return (
    <Box sx={{ width: 300 }}>
      <Slider
        aria-label="Restricted values"
        defaultValue={20}
        getAriaValueText={valuetext}
        step={null}
        valueLabelDisplay="auto"
        marks={marks}
      />
    </Box>
  );
}
marks <- list(
  list(
    value = 0,
    label = "0°C"
  ),
  list(
    value = 20,
    label = "20°C"
  ),
  list(
    value = 37,
    label = "37°C"
  ),
  list(
    value = 100,
    label = "100°C"
  )
)

ui <- CssBaseline(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "restrictedValuesSlider", value = 20, 
                      "aria-label" = "Restricted values", 
                      getAriaValueText = JS("function(value) { return `${value} °C`; }"),
                      step = NULL,
                      valueLabelDisplay = "auto",
                      marks = marks)
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Label always visible

You can force the thumb label to be always visible with valueLabelDisplay=“on”.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

const marks = [
  {
    value: 0,
    label: '0°C',
  },
  {
    value: 20,
    label: '20°C',
  },
  {
    value: 37,
    label: '37°C',
  },
  {
    value: 100,
    label: '100°C',
  },
];

function valuetext(value: number) {
  return `${value}°C`;
}

export default function DiscreteSliderLabel() {
  return (
    <Box sx={{ width: 300 }}>
      <Slider
        aria-label="Always visible"
        defaultValue={80}
        getAriaValueText={valuetext}
        step={10}
        marks={marks}
        valueLabelDisplay="on"
      />
    </Box>
  );
}
marks <- list(
  list(
    value = 0,
    label = "0°C"
  ),
  list(
    value = 20,
    label = "20°C"
  ),
  list(
    value = 37,
    label = "37°C"
  ),
  list(
    value = 100,
    label = "100°C"
  )
)

ui <- CssBaseline(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "alwaysVisibleSlider", value = 80, 
                      "aria-label" = "Always visible", 
                      getAriaValueText = JS("function(value) { return `${value} °C`; }"),
                      step = 10,
                      marks = marks,
                      valueLabelDisplay = "on")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Range slider

The slider can be used to set the start and end of a range by supplying an array of values to the value prop.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

function valuetext(value: number) {
  return `${value}°C`;
}

export default function RangeSlider() {
  const [value, setValue] = React.useState<number[]>([20, 37]);

  const handleChange = (event: Event, newValue: number[]) => {
    setValue(newValue);
  };

  return (
    <Box sx={{ width: 300 }}>
      <Slider
        getAriaLabel={() => 'Temperature range'}
        value={value}
        onChange={handleChange}
        valueLabelDisplay="auto"
        getAriaValueText={valuetext}
      />
    </Box>
  );
}
ui <- shinyMaterialUIPage(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(
      inputId = "rangeSlider", 
      value = c(20, 37), 
      getAriaLabel = JS('function() { "Temperature range" }'), 
      valueLabelDisplay = "auto",
      getAriaValueText = JS("function(value) { return `${value} °C`; }"),
    )
  )
)

server <- function(input, output, session) {
  observeEvent(input$rangeSlider, {
    # Handle the range slider value change
  })
}

shinyApp(ui, server)

Minimum distance

You can enforce a minimum distance between values in the onChange event handler. By default, when you move the pointer over a thumb while dragging another thumb, the active thumb will swap to the hovered thumb. You can disable this behavior with the disableSwap prop. If you want the range to shift when reaching minimum distance, you can utilize the activeThumb parameter in onChange.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

function valuetext(value: number) {
  return `${value}°C`;
}

const minDistance = 10;

export default function MinimumDistanceSlider() {
  const [value1, setValue1] = React.useState<number[]>([20, 37]);

  const handleChange1 = (event: Event, newValue: number[], activeThumb: number) => {
    if (activeThumb === 0) {
      setValue1([Math.min(newValue[0], value1[1] - minDistance), value1[1]]);
    } else {
      setValue1([value1[0], Math.max(newValue[1], value1[0] + minDistance)]);
    }
  };

  const [value2, setValue2] = React.useState<number[]>([20, 37]);

  const handleChange2 = (event: Event, newValue: number[], activeThumb: number) => {
    if (newValue[1] - newValue[0] < minDistance) {
      if (activeThumb === 0) {
        const clamped = Math.min(newValue[0], 100 - minDistance);
        setValue2([clamped, clamped + minDistance]);
      } else {
        const clamped = Math.max(newValue[1], minDistance);
        setValue2([clamped - minDistance, clamped]);
      }
    } else {
      setValue2(newValue);
    }
  };

  return (
    <Box sx={{ width: 300 }}>
      <Slider
        getAriaLabel={() => 'Minimum distance'}
        value={value1}
        onChange={handleChange1}
        valueLabelDisplay="auto"
        getAriaValueText={valuetext}
        disableSwap
      />
      <Slider
        getAriaLabel={() => 'Minimum distance shift'}
        value={value2}
        onChange={handleChange2}
        valueLabelDisplay="auto"
        getAriaValueText={valuetext}
        disableSwap
      />
    </Box>
  );
}
minDistance <- 10
ui <- shinyMaterialUIPage(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "minDistanceSlider1", value = c(20, 37), 
                      getAriaLabel = JS('function() { return "Minimum distance" }'), 
                      valueLabelDisplay = "auto",
                      disableSwap = TRUE),
    Slider.shinyInput(inputId = "minDistanceSlider2", value = c(20, 37), 
                      getAriaLabel = JS('function() { return "Minimum distance shift" }'), 
                      valueLabelDisplay = "auto",
                      disableSwap = TRUE)
  )
)
server <- function(input, output, session) {
  # Create reactive values to track when updates are in progress
  updating <- reactiveValues(slider1 = FALSE, slider2 = FALSE)
  
  # Handler for first slider
  observeEvent(input$minDistanceSlider1, {
    # Skip if this is an update we triggered
    if (updating$slider1) return()
    
    updating$slider1 <- TRUE
    on.exit(updating$slider1 <- FALSE)
    
    values <- input$minDistanceSlider1
    if (length(values) >= 2) {
      # Get active thumb (default to left/0 if NULL)
      active <- if (is.null(input$minDistanceSlider1_activeThumb)) 0 else input$minDistanceSlider1_activeThumb
      
      # Calculate new values based on minimum distance
      if (active == 0) {
        # Left thumb moved - ensure min distance from right
        left <- min(values[1], values[2] - minDistance)
        updateSlider.shinyInput(session, "minDistanceSlider1", value = c(left, values[2]))
      } else {
        # Right thumb moved - ensure min distance from left
        right <- max(values[2], values[1] + minDistance)
        updateSlider.shinyInput(session, "minDistanceSlider1", value = c(values[1], right))
      }
    }
  })
  
  # Handler for second slider
  observeEvent(input$minDistanceSlider2, {
    # Skip if this is an update we triggered
    if (updating$slider2) return()
    
    updating$slider2 <- TRUE
    on.exit(updating$slider2 <- FALSE)
    
    values <- input$minDistanceSlider2
    if (length(values) >= 2 && values[2] - values[1] < minDistance) {
      # Get active thumb (default to left/0 if NULL)
      active <- if (is.null(input$minDistanceSlider2_activeThumb)) 0 else input$minDistanceSlider2_activeThumb
      
      if (active == 0) {
        # Left thumb moved - clamp and update right accordingly
        left <- min(values[1], 100 - minDistance)
        updateSlider.shinyInput(session, "minDistanceSlider2", value = c(left, left + minDistance))
      } else {
        # Right thumb moved - clamp and update left accordingly
        right <- max(values[2], minDistance)
        updateSlider.shinyInput(session, "minDistanceSlider2", value = c(right - minDistance, right))
      }
    }
  })
}

shinyApp(ui, server)

Slider with input field

In this example, an input allows a discrete value to be set.

JS code
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import Slider from '@mui/material/Slider';
import MuiInput from '@mui/material/Input';
import VolumeUp from '@mui/icons-material/VolumeUp';

const Input = styled(MuiInput)`
  width: 42px;
`;

export default function InputSlider() {
  const [value, setValue] = React.useState(30);

  const handleSliderChange = (event: Event, newValue: number) => {
    setValue(newValue);
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue(event.target.value === '' ? 0 : Number(event.target.value));
  };

  const handleBlur = () => {
    if (value < 0) {
      setValue(0);
    } else if (value > 100) {
      setValue(100);
    }
  };

  return (
    <Box sx={{ width: 250 }}>
      <Typography id="input-slider" gutterBottom>
        Volume
      </Typography>
      <Grid container spacing={2} sx={{ alignItems: 'center' }}>
        <Grid>
          <VolumeUp />
        </Grid>
        <Grid size="grow">
          <Slider
            value={typeof value === 'number' ? value : 0}
            onChange={handleSliderChange}
            aria-labelledby="input-slider"
          />
        </Grid>
        <Grid>
          <Input
            value={value}
            size="small"
            onChange={handleInputChange}
            onBlur={handleBlur}
            inputProps={{
              step: 10,
              min: 0,
              max: 100,
              type: 'number',
              'aria-labelledby': 'input-slider',
            }}
          />
        </Grid>
      </Grid>
    </Box>
  );
}
ui <- shinyMaterialUIPage(
  Box(sx = list(width = 250, mt = 4),
    Typography(id = "input-slider", gutterBottom = TRUE,
      "Volume"
    ),
    Grid(container = TRUE, spacing = 2, sx = list(alignItems = "center"),
      Grid(
        shiny::icon("volume-up")
      ),
      Grid(size = "grow",
        Slider.shinyInput(inputId = "volumeSlider", value = 30,
                          "aria-labelledby" = "input-slider")
      ),
      Grid(
        Input.shinyInput(inputId = "volumeInput", value = 30, size = "small",
                          inputProps = list(
                            step = 10,
                            min = 0,
                            max = 100,
                            type = "number",
                            "aria-labelledby" = "input-slider"
                          ))
      )
    )
  )
)

server <- function(input, output, session) {
  # Update slider when input changes
  observeEvent(input$volumeInput, {
    value <- if(input$volumeInput == "") 0 else as.numeric(input$volumeInput)
    # Clamp value between 0 and 100
    value <- max(0, min(100, value))
    updateSlider.shinyInput(session, "volumeSlider", value = value)
  })
  
  # Update input when slider changes
  observeEvent(input$volumeSlider, {
    updateInput.shinyInput(session, "volumeInput", value = input$volumeSlider)
  })
}

shinyApp(ui, server)

Color

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';

function valuetext(value: number) {
  return `${value}°C`;
}

export default function ColorSlider() {
  return (
    <Box sx={{ width: 300 }}>
      <Slider
        aria-label="Temperature"
        defaultValue={30}
        getAriaValueText={valuetext}
        color="secondary"
      />
    </Box>
  );
}
ui <- CssBaseline(
  Box(sx = list(width = 300, mt = 4),
    Slider.shinyInput(inputId = "colorSlider", value = 30, 
                      "aria-label" = "Temperature", 
                      getAriaValueText = JS("function(value) { return `${value} °C`; }"),
                      color = "secondary")
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Customization

Here are some examples of customizing the component. You can learn more about this in the overrides documentation page.

JS code
import * as React from 'react';
import Slider, { SliderThumb, SliderValueLabelProps } from '@mui/material/Slider';
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import Box from '@mui/material/Box';

function ValueLabelComponent(props: SliderValueLabelProps) {
  const { children, value } = props;

  return (
    <Tooltip enterTouchDelay={0} placement="top" title={value}>
      {children}
    </Tooltip>
  );
}

const iOSBoxShadow =
  '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.13),0 0 0 1px rgba(0,0,0,0.02)';

const IOSSlider = styled(Slider)(({ theme }) => ({
  color: '#007bff',
  height: 5,
  padding: '15px 0',
  '& .MuiSlider-thumb': {
    height: 20,
    width: 20,
    backgroundColor: '#fff',
    boxShadow: '0 0 2px 0px rgba(0, 0, 0, 0.1)',
    '&:focus, &:hover, &.Mui-active': {
      boxShadow: '0px 0px 3px 1px rgba(0, 0, 0, 0.1)',
      // Reset on touch devices, it doesn't add specificity
      '@media (hover: none)': {
        boxShadow: iOSBoxShadow,
      },
    },
    '&:before': {
      boxShadow:
        '0px 0px 1px 0px rgba(0,0,0,0.2), 0px 0px 0px 0px rgba(0,0,0,0.14), 0px 0px 1px 0px rgba(0,0,0,0.12)',
    },
  },
  '& .MuiSlider-valueLabel': {
    fontSize: 12,
    fontWeight: 'normal',
    top: -6,
    backgroundColor: 'unset',
    color: theme.palette.text.primary,
    '&::before': {
      display: 'none',
    },
    '& *': {
      background: 'transparent',
      color: '#000',
      ...theme.applyStyles('dark', {
        color: '#fff',
      }),
    },
  },
  '& .MuiSlider-track': {
    border: 'none',
    height: 5,
  },
  '& .MuiSlider-rail': {
    opacity: 0.5,
    boxShadow: 'inset 0px 0px 4px -2px #000',
    backgroundColor: '#d0d0d0',
  },
  ...theme.applyStyles('dark', {
    color: '#0a84ff',
  }),
}));

const PrettoSlider = styled(Slider)({
  color: '#52af77',
  height: 8,
  '& .MuiSlider-track': {
    border: 'none',
  },
  '& .MuiSlider-thumb': {
    height: 24,
    width: 24,
    backgroundColor: '#fff',
    border: '2px solid currentColor',
    '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': {
      boxShadow: 'inherit',
    },
    '&::before': {
      display: 'none',
    },
  },
  '& .MuiSlider-valueLabel': {
    lineHeight: 1.2,
    fontSize: 12,
    background: 'unset',
    padding: 0,
    width: 32,
    height: 32,
    borderRadius: '50% 50% 50% 0',
    backgroundColor: '#52af77',
    transformOrigin: 'bottom left',
    transform: 'translate(50%, -100%) rotate(-45deg) scale(0)',
    '&::before': { display: 'none' },
    '&.MuiSlider-valueLabelOpen': {
      transform: 'translate(50%, -100%) rotate(-45deg) scale(1)',
    },
    '& > *': {
      transform: 'rotate(45deg)',
    },
  },
});

const AirbnbSlider = styled(Slider)(({ theme }) => ({
  color: '#3a8589',
  height: 3,
  padding: '13px 0',
  '& .MuiSlider-thumb': {
    height: 27,
    width: 27,
    backgroundColor: '#fff',
    border: '1px solid currentColor',
    '&:hover': {
      boxShadow: '0 0 0 8px rgba(58, 133, 137, 0.16)',
    },
    '& .airbnb-bar': {
      height: 9,
      width: 1,
      backgroundColor: 'currentColor',
      marginLeft: 1,
      marginRight: 1,
    },
  },
  '& .MuiSlider-track': {
    height: 3,
  },
  '& .MuiSlider-rail': {
    color: '#d8d8d8',
    opacity: 1,
    height: 3,
    ...theme.applyStyles('dark', {
      color: '#bfbfbf',
      opacity: undefined,
    }),
  },
}));

interface AirbnbThumbComponentProps extends React.HTMLAttributes<unknown> {}

function AirbnbThumbComponent(props: AirbnbThumbComponentProps) {
  const { children, ...other } = props;
  return (
    <SliderThumb {...other}>
      {children}
      <span className="airbnb-bar" />
      <span className="airbnb-bar" />
      <span className="airbnb-bar" />
    </SliderThumb>
  );
}

export default function CustomizedSlider() {
  return (
    <Box sx={{ width: 320 }}>
      <Typography gutterBottom>iOS</Typography>
      <IOSSlider aria-label="ios slider" defaultValue={60} valueLabelDisplay="on" />
      <Box sx={{ m: 3 }} />
      <Typography gutterBottom>pretto.fr</Typography>
      <PrettoSlider
        valueLabelDisplay="auto"
        aria-label="pretto slider"
        defaultValue={20}
      />
      <Box sx={{ m: 3 }} />
      <Typography gutterBottom>Tooltip value label</Typography>
      <Slider
        valueLabelDisplay="auto"
        slots={{
          valueLabel: ValueLabelComponent,
        }}
        aria-label="custom thumb label"
        defaultValue={20}
      />
      <Box sx={{ m: 3 }} />
      <Typography gutterBottom>Airbnb</Typography>
      <AirbnbSlider
        slots={{ thumb: AirbnbThumbComponent }}
        getAriaLabel={(index) => (index === 0 ? 'Minimum price' : 'Maximum price')}
        defaultValue={[20, 40]}
      />
    </Box>
  );
}
ui <- shinyMaterialUIPage(
  CssBaseline(
    Box(
      sx = list(width = 320),
      Typography("iOS", gutterBottom = TRUE),
      ThemeProvider(
        theme = list(
          palette = list(
            primary = list(main = "#007bff")
          ),
          components = list(
            MuiSlider = list(
              styleOverrides = list(
                root = list(
                  height = 5,
                  padding = "15px 0"
                ),
                thumb = list(
                  height = 20,
                  width = 20,
                  backgroundColor = "#fff",
                  boxShadow = "0 0 2px 0px rgba(0, 0, 0, 0.1)"
                ),
                track = list(
                  border = "none",
                  height = 5
                ),
                rail = list(
                  opacity = 0.5,
                  boxShadow = "inset 0px 0px 4px -2px #000",
                  backgroundColor = "#d0d0d0"
                )
              )
            )
          )
        ),
        Slider.shinyInput(
          inputId = "iosSlider",
          value = 60,
          "aria-label" = "ios slider",
          valueLabelDisplay = "on"
        )
      ),
      Box(sx = list(m = 3)),
      
      Typography("pretto.fr", gutterBottom = TRUE),
      ThemeProvider(
        theme = list(
          palette = list(
            primary = list(main = "#52af77")
          ),
          components = list(
            MuiSlider = list(
              styleOverrides = list(
                root = list(
                  color = '#52af77',
                  height = 8,
                  '& .MuiSlider-track' = list(
                    border = 'none'
                  ),
                  '& .MuiSlider-thumb' = list(
                    height = 24,
                    width = 24,
                    backgroundColor = '#fff',
                    border = '2px solid currentColor',
                    '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible' = list(
                      boxShadow = 'inherit'
                    ),
                    '&::before' = list(
                      display = 'none'
                    )
                  ),
                  '& .MuiSlider-valueLabel' = list(
                    lineHeight = 1.2,
                    fontSize = 12,
                    background = 'unset',
                    padding = 0,
                    width = 32,
                    height = 32,
                    borderRadius = '50% 50% 50% 0',
                    backgroundColor = '#52af77',
                    transformOrigin = 'bottom left',
                    transform = 'translate(50%, -100%) rotate(-45deg) scale(0)',
                    '&::before' = list( display = 'none' ),
                    '&.MuiSlider-valueLabelOpen' = list(
                      transform = 'translate(50%, -100%) rotate(-45deg) scale(1)'
                    ),
                    '& > *' = list(
                      transform = 'rotate(45deg)'
                    )
                  )
                )
              )
            )
          )
        ),
        Slider.shinyInput(
          inputId = "prettoSlider",
          value = 20,
          "aria-label" = "pretto slider",
          valueLabelDisplay = "auto"
        )
      ),
      Box(sx = list(m = 3)),
      
      Typography("Tooltip value label", gutterBottom = TRUE),
      Slider.shinyInput(
        inputId = "tooltipSlider",
        value = 20,
        "aria-label" = "custom thumb label",
        valueLabelDisplay = "auto"
      ),
      Box(sx = list(m = 3)),
      
      Typography("Airbnb", gutterBottom = TRUE),
      ThemeProvider(
        theme = list(
          palette = list(
            primary = list(main = "#3a8589")
          ),
          components = list(
            MuiSlider = list(
              styleOverrides = list(
                root = list(
                  height = 3,
                  padding = "13px 0"
                ),
                thumb = list(
                  height = 27,
                  width = 27,
                  backgroundColor = "#fff",
                  border = "1px solid currentColor"
                ),
                track = list(height = 3),
                rail = list(
                  color = "#d8d8d8",
                  opacity = 1,
                  height = 3
                )
              )
            )
          )
        ),
        Slider.shinyInput(
          inputId = "airbnbSlider", 
          value = c(20, 40)
        )
      )
    )
  )
)

server <- function(input, output, session) {
  # Observe slider values for handling
  observe({
    print(paste("iOS Slider value:", input$iosSlider))
    print(paste("Pretto Slider value:", input$prettoSlider))
    print(paste("Tooltip Slider value:", input$tooltipSlider))
    print(paste("Airbnb Slider value:", input$airbnbSlider))
  })
}

shinyApp(ui, server)

Music player

JS code
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Slider from '@mui/material/Slider';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import PauseRounded from '@mui/icons-material/PauseRounded';
import PlayArrowRounded from '@mui/icons-material/PlayArrowRounded';
import FastForwardRounded from '@mui/icons-material/FastForwardRounded';
import FastRewindRounded from '@mui/icons-material/FastRewindRounded';
import VolumeUpRounded from '@mui/icons-material/VolumeUpRounded';
import VolumeDownRounded from '@mui/icons-material/VolumeDownRounded';

const WallPaper = styled('div')({
  position: 'absolute',
  width: '100%',
  height: '100%',
  top: 0,
  left: 0,
  overflow: 'hidden',
  background: 'linear-gradient(rgb(255, 38, 142) 0%, rgb(255, 105, 79) 100%)',
  transition: 'all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s',
  '&::before': {
    content: '""',
    width: '140%',
    height: '140%',
    position: 'absolute',
    top: '-40%',
    right: '-50%',
    background:
      'radial-gradient(at center center, rgb(62, 79, 249) 0%, rgba(62, 79, 249, 0) 64%)',
  },
  '&::after': {
    content: '""',
    width: '140%',
    height: '140%',
    position: 'absolute',
    bottom: '-50%',
    left: '-30%',
    background:
      'radial-gradient(at center center, rgb(247, 237, 225) 0%, rgba(247, 237, 225, 0) 70%)',
    transform: 'rotate(30deg)',
  },
});

const Widget = styled('div')(({ theme }) => ({
  padding: 16,
  borderRadius: 16,
  width: 343,
  maxWidth: '100%',
  margin: 'auto',
  position: 'relative',
  zIndex: 1,
  backgroundColor: 'rgba(255,255,255,0.4)',
  backdropFilter: 'blur(40px)',
  ...theme.applyStyles('dark', {
    backgroundColor: 'rgba(0,0,0,0.6)',
  }),
}));

const CoverImage = styled('div')({
  width: 100,
  height: 100,
  objectFit: 'cover',
  overflow: 'hidden',
  flexShrink: 0,
  borderRadius: 8,
  backgroundColor: 'rgba(0,0,0,0.08)',
  '& > img': {
    width: '100%',
  },
});

const TinyText = styled(Typography)({
  fontSize: '0.75rem',
  opacity: 0.38,
  fontWeight: 500,
  letterSpacing: 0.2,
});

export default function MusicPlayerSlider() {
  const duration = 200; // seconds
  const [position, setPosition] = React.useState(32);
  const [paused, setPaused] = React.useState(false);
  function formatDuration(value: number) {
    const minute = Math.floor(value / 60);
    const secondLeft = value - minute * 60;
    return `${minute}:${secondLeft < 10 ? `0${secondLeft}` : secondLeft}`;
  }
  return (
    <Box sx={{ width: '100%', overflow: 'hidden', position: 'relative', p: 3 }}>
      <Widget>
        <Box sx={{ display: 'flex', alignItems: 'center' }}>
          <CoverImage>
            <img
              alt="can't win - Chilling Sunday"
              src="/static/images/sliders/chilling-sunday.jpg"
            />
          </CoverImage>
          <Box sx={{ ml: 1.5, minWidth: 0 }}>
            <Typography
              variant="caption"
              sx={{ color: 'text.secondary', fontWeight: 500 }}
            >
              Jun Pulse
            </Typography>
            <Typography noWrap>
              <b>คนเก่าเขาทำไว้ดี (Can&apos;t win)</b>
            </Typography>
            <Typography noWrap sx={{ letterSpacing: -0.25 }}>
              Chilling Sunday &mdash; คนเก่าเขาทำไว้ดี
            </Typography>
          </Box>
        </Box>
        <Slider
          aria-label="time-indicator"
          size="small"
          value={position}
          min={0}
          step={1}
          max={duration}
          onChange={(_, value) => setPosition(value)}
          sx={(t) => ({
            color: 'rgba(0,0,0,0.87)',
            height: 4,
            '& .MuiSlider-thumb': {
              width: 8,
              height: 8,
              transition: '0.3s cubic-bezier(.47,1.64,.41,.8)',
              '&::before': {
                boxShadow: '0 2px 12px 0 rgba(0,0,0,0.4)',
              },
              '&:hover, &.Mui-focusVisible': {
                boxShadow: `0px 0px 0px 8px ${'rgb(0 0 0 / 16%)'}`,
                ...t.applyStyles('dark', {
                  boxShadow: `0px 0px 0px 8px ${'rgb(255 255 255 / 16%)'}`,
                }),
              },
              '&.Mui-active': {
                width: 20,
                height: 20,
              },
            },
            '& .MuiSlider-rail': {
              opacity: 0.28,
            },
            ...t.applyStyles('dark', {
              color: '#fff',
            }),
          })}
        />
        <Box
          sx={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            mt: -2,
          }}
        >
          <TinyText>{formatDuration(position)}</TinyText>
          <TinyText>-{formatDuration(duration - position)}</TinyText>
        </Box>
        <Box
          sx={(theme) => ({
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            mt: -1,
            '& svg': {
              color: '#000',
              ...theme.applyStyles('dark', {
                color: '#fff',
              }),
            },
          })}
        >
          <IconButton aria-label="previous song">
            <FastRewindRounded fontSize="large" />
          </IconButton>
          <IconButton
            aria-label={paused ? 'play' : 'pause'}
            onClick={() => setPaused(!paused)}
          >
            {paused ? (
              <PlayArrowRounded sx={{ fontSize: '3rem' }} />
            ) : (
              <PauseRounded sx={{ fontSize: '3rem' }} />
            )}
          </IconButton>
          <IconButton aria-label="next song">
            <FastForwardRounded fontSize="large" />
          </IconButton>
        </Box>
        <Stack
          spacing={2}
          direction="row"
          sx={(theme) => ({
            mb: 1,
            px: 1,
            '& > svg': {
              color: 'rgba(0,0,0,0.4)',
              ...theme.applyStyles('dark', {
                color: 'rgba(255,255,255,0.4)',
              }),
            },
          })}
          alignItems="center"
        >
          <VolumeDownRounded />
          <Slider
            aria-label="Volume"
            defaultValue={30}
            sx={(t) => ({
              color: 'rgba(0,0,0,0.87)',
              '& .MuiSlider-track': {
                border: 'none',
              },
              '& .MuiSlider-thumb': {
                width: 24,
                height: 24,
                backgroundColor: '#fff',
                '&::before': {
                  boxShadow: '0 4px 8px rgba(0,0,0,0.4)',
                },
                '&:hover, &.Mui-focusVisible, &.Mui-active': {
                  boxShadow: 'none',
                },
              },
              ...t.applyStyles('dark', {
                color: '#fff',
              }),
            })}
          />
          <VolumeUpRounded />
        </Stack>
      </Widget>
      <WallPaper />
    </Box>
  );
}
# Format duration function
formatDuration <- function(value) {
  minute <- floor(value / 60)
  secondLeft <- value - minute * 60
  paste0(minute, ":", ifelse(secondLeft < 10, paste0("0", secondLeft), secondLeft))
}

ui <- shinyMaterialUIPage(
  tags$style("
    .wall-paper {
      position: absolute;
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      overflow: hidden;
      background: linear-gradient(rgb(255, 38, 142) 0%, rgb(255, 105, 79) 100%);
      transition: all 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s;
    }
    .music-widget {
      padding: 16px;
      border-radius: 16px;
      width: 343px;
      max-width: 100%;
      margin: auto;
      position: relative;
      z-index: 1;
      background-color: rgba(255,255,255,0.4);
      backdrop-filter: blur(40px);
    }
    .cover-image {
      width: 100px;
      height: 100px;
      object-fit: cover;
      overflow: hidden;
      flex-shrink: 0;
      border-radius: 8px;
      background-color: rgba(0,0,0,0.08);
    }
    .cover-image img {
      width: 100%;
    }
    .tiny-text {
      font-size: 0.75rem;
      opacity: 0.38;
      font-weight: 500;
      letter-spacing: 0.2px;
    }
  "),
  
  CssBaseline(
    Box(
      sx = list(width = "100%", overflow = "hidden", position = "relative", p = 3),
      
      div(class = "music-widget",
        Box(
          sx = list(display = "flex", alignItems = "center"),
          div(class = "cover-image",
            tags$img(
              alt = "can't win - Chilling Sunday",
              src = "https://mui.com/static/images/sliders/chilling-sunday.jpg"
            )
          ),
          Box(
            sx = list(ml = 1.5, minWidth = 0),
            Typography(
              variant = "caption",
              sx = list(color = "text.secondary", fontWeight = 500),
              "Jun Pulse"
            ),
            Typography(
              noWrap = TRUE,
              tags$b("คนเก่าเขาทำไว้ดี (Can't win)")
            ),
            Typography(
              noWrap = TRUE,
              sx = list(letterSpacing = -0.25),
              "Chilling Sunday — คนเก่าเขาทำไว้ดี"
            )
          )
        ),
        
        Slider.shinyInput(
          inputId = "timeSlider",
          value = 32,
          min = 0,
          step = 1,
          max = 200,
          size = "small",
          "aria-label" = "time-indicator"
        ),
        
        Box(
          sx = list(
            display = "flex", 
            alignItems = "center", 
            justifyContent = "space-between",
            mt = -2
          ),
          div(class = "tiny-text", textOutput("currentPosition")),
          div(class = "tiny-text", textOutput("remainingTime"))
        ),
        
        Box(
          sx = list(
            display = "flex",
            alignItems = "center",
            justifyContent = "center",
            mt = -1
          ),
          IconButton(
            "aria-label" = "previous song",
            shiny::icon("backward")
          ),
          IconButton(
            inputId = "playPauseBtn",
            "aria-label" = "play",
            shiny::icon("play", style = "font-size: 2rem;")
          ),
          IconButton(
            "aria-label" = "next song",
            shiny::icon("forward")
          )
        ),
        
        Stack(
          spacing = 2,
          direction = "row",
          sx = list(mb = 1, px = 1),
          alignItems = "center",
          
          shiny::icon("volume-down"),
          
          Slider.shinyInput(
            inputId = "volumeSlider",
            value = 30,
            "aria-label" = "Volume",
            sx = list(
              width = 200,
              color = "rgba(0,0,0,0.87)"
            )
          ),
          
          shiny::icon("volume-up")
        )
      ),
      
      div(class = "wall-paper")
    )
  )
)

server <- function(input, output, session) {
  # Track playing state
  playing <- reactiveVal(TRUE)
  
  # Format time displays
  output$currentPosition <- renderText({
    formatDuration(input$timeSlider)
  })
  
  output$remainingTime <- renderText({
    paste0("-", formatDuration(200 - input$timeSlider))
  })
  
  # Handle play/pause button
  observeEvent(input$playPauseBtn, {
    current <- playing()
    playing(!current)
    
    # Update button icon
    if (!current) {
      updateActionButton(session, "playPauseBtn", 
                         icon = shiny::icon("play", style = "font-size: 2rem;"))
    } else {
      updateActionButton(session, "playPauseBtn", 
                         icon = shiny::icon("pause", style = "font-size: 2rem;"))
    }
  })
  
  # For a real app, you would implement audio playback functionality here
}

shinyApp(ui, server)

Vertical sliders

Set the orientation prop to “vertical” to create vertical sliders. The thumb will track vertical movement instead of horizontal movement.

JS code
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Slider from '@mui/material/Slider';

export default function VerticalSlider() {
  return (
    <Stack sx={{ height: 300 }} spacing={1} direction="row">
      <Slider
        aria-label="Temperature"
        orientation="vertical"
        getAriaValueText={getAriaValueText}
        valueLabelDisplay="auto"
        defaultValue={30}
      />
      <Slider
        aria-label="Temperature"
        orientation="vertical"
        defaultValue={30}
        valueLabelDisplay="auto"
        disabled
      />
      <Slider
        getAriaLabel={() => 'Temperature'}
        orientation="vertical"
        getAriaValueText={getAriaValueText}
        defaultValue={[20, 37]}
        valueLabelDisplay="auto"
        marks={marks}
      />
    </Stack>
  );
}

function getAriaValueText(value: number) {
  return `${value}°C`;
}

const marks = [
  {
    value: 0,
    label: '0°C',
  },
  {
    value: 20,
    label: '20°C',
  },
  {
    value: 37,
    label: '37°C',
  },
  {
    value: 100,
    label: '100°C',
  },
];
# Define marks for the slider
marks <- list(
  list(value = 0, label = "0°C"),
  list(value = 20, label = "20°C"),
  list(value = 37, label = "37°C"),
  list(value = 100, label = "100°C")
)

ui <- CssBaseline(
  Stack(
    sx = list(height = 300, mt = 4, ml = 4),
    spacing = 1,
    direction = "row",
    
    Slider.shinyInput(
      inputId = "verticalSlider1",
      orientation = "vertical",
      valueLabelDisplay = "auto",
      value = 30,
      "aria-label" = "Temperature"
    ),
    
    Slider.shinyInput(
      inputId = "verticalSlider2",
      orientation = "vertical",
      valueLabelDisplay = "auto",
      value = 30,
      disabled = TRUE,
      "aria-label" = "Temperature"
    ),
    
    Slider.shinyInput(
      inputId = "verticalSlider3",
      orientation = "vertical",
      valueLabelDisplay = "auto",
      value = c(20, 37),
      marks = marks
    )
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Chrome versions below 124 implement aria-orientation incorrectly for vertical sliders and expose them as ‘horizontal’ in the accessibility tree. (Chromium issue #40736841)

The -webkit-appearance: slider-vertical CSS property can be used to correct this for these older versions, with the trade-off of causing a console warning in newer Chrome versions:

.MuiSlider-thumb input {
  -webkit-appearance: slider-vertical;
}

Marks placement

You can customize your slider by adding and repositioning marks for minimum and maximum values.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Slider from '@mui/material/Slider';
import Typography from '@mui/material/Typography';

const MAX = 100;
const MIN = 0;
const marks = [
  {
    value: MIN,
    label: '',
  },
  {
    value: MAX,
    label: '',
  },
];

export default function CustomMarks() {
  const [val, setVal] = React.useState<number>(MIN);
  const handleChange = (_: Event, newValue: number) => {
    setVal(newValue);
  };

  return (
    <Box sx={{ width: 250 }}>
      <Slider
        marks={marks}
        step={10}
        value={val}
        valueLabelDisplay="auto"
        min={MIN}
        max={MAX}
        onChange={handleChange}
      />
      <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
        <Typography
          variant="body2"
          onClick={() => setVal(MIN)}
          sx={{ cursor: 'pointer' }}
        >
          {MIN} min
        </Typography>
        <Typography
          variant="body2"
          onClick={() => setVal(MAX)}
          sx={{ cursor: 'pointer' }}
        >
          {MAX} max
        </Typography>
      </Box>
    </Box>
  );
}
# Define constants
MAX <- 100
MIN <- 0
marks <- list(
  list(
    value = MIN,
    label = ""
  ),
  list(
    value = MAX,
    label = ""
  )
)

# Create Shiny app
ui <- shinyMaterialUIPage(
  CssBaseline(
    Box(
      sx = list(width = 250, m = 4),
      Slider.shinyInput(
        inputId = "custom_slider",
        marks = marks,
        step = 10,
        value = MIN,
        valueLabelDisplay = "auto",
        min = MIN,
        max = MAX
      ),
      Box(
        sx = list(display = "flex", justifyContent = "space-between"),
        Typography(
          variant = "body2",
          sx = list(cursor = "pointer"),
          paste0(MIN, " min")
        ),
        Typography(
          variant = "body2",
          sx = list(cursor = "pointer"),
          paste0(MAX, " max")
        )
      )
    )
  )
)

server <- function(input, output, session) {}

shinyApp(ui, server)

Track

The track shows the range available for user selection. Removed track

The track can be turned off with track=FALSE.

Removed track

JS code
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Slider from '@mui/material/Slider';

const Separator = styled('div')(
  ({ theme }) => `
  height: ${theme.spacing(3)};
`,
);

const marks = [
  {
    value: 0,
    label: '0°C',
  },
  {
    value: 20,
    label: '20°C',
  },
  {
    value: 37,
    label: '37°C',
  },
  {
    value: 100,
    label: '100°C',
  },
];

function valuetext(value: number) {
  return `${value}°C`;
}

export default function TrackFalseSlider() {
  return (
    <Box sx={{ width: 250 }}>
      <Typography id="track-false-slider" gutterBottom>
        Removed track
      </Typography>
      <Slider
        track={false}
        aria-labelledby="track-false-slider"
        getAriaValueText={valuetext}
        defaultValue={30}
        marks={marks}
      />
      <Separator />
      <Typography id="track-false-range-slider" gutterBottom>
        Removed track range slider
      </Typography>
      <Slider
        track={false}
        aria-labelledby="track-false-range-slider"
        getAriaValueText={valuetext}
        defaultValue={[20, 37, 50]}
        marks={marks}
      />
    </Box>
  );
}
# Define data and helper functions
marks <- list(
  list(value = 0, label = "0°C"),
  list(value = 20, label = "20°C"),
  list(value = 37, label = "37°C"),
  list(value = 100, label = "100°C")
)

# Create Shiny app
ui <- shinyMaterialUIPage(
  CssBaseline(
    Box(
      sx = list(width = 250),
      Typography(
        id = "track-false-slider",
        gutterBottom = TRUE,
        "Removed track"
      ),
      Slider.shinyInput(
        inputId = "track_false_slider",
        track = FALSE,
        `aria-labelledby` = "track-false-slider",
        #getAriaValueText = "",
        value = 30,
        marks = marks
      ),
      # Custom separator with theme spacing
      div(style = "height: 24px;"),
      Typography(
        id = "track-false-range-slider",
        gutterBottom = TRUE,
        "Removed track range slider"
      ),
      Slider.shinyInput(
        inputId = "track_false_range_slider",
        track = FALSE,
        `aria-labelledby` = "track-false-range-slider",
        #getAriaValueText = "",
        value = c(20, 37, 50),
        marks = marks
      )
    )
  )
)

server <- function(input, output, session) {
  # Empty server since this is a display-only example
}

shinyApp(ui, server)

Inverted track

The track can be inverted with track=“inverted”.

Inverted track

JS code
import * as React from 'react';
import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Slider from '@mui/material/Slider';

const Separator = styled('div')(
  ({ theme }) => `
  height: ${theme.spacing(3)};
`,
);

const marks = [
  {
    value: 0,
    label: '0°C',
  },
  {
    value: 20,
    label: '20°C',
  },
  {
    value: 37,
    label: '37°C',
  },
  {
    value: 100,
    label: '100°C',
  },
];

function valuetext(value: number) {
  return `${value}°C`;
}

export default function TrackInvertedSlider() {
  return (
    <Box sx={{ width: 250 }}>
      <Typography id="track-inverted-slider" gutterBottom>
        Inverted track
      </Typography>
      <Slider
        track="inverted"
        aria-labelledby="track-inverted-slider"
        getAriaValueText={valuetext}
        defaultValue={30}
        marks={marks}
      />
      <Separator />
      <Typography id="track-inverted-range-slider" gutterBottom>
        Inverted track range
      </Typography>
      <Slider
        track="inverted"
        aria-labelledby="track-inverted-range-slider"
        getAriaValueText={valuetext}
        defaultValue={[20, 37]}
        marks={marks}
      />
    </Box>
  );
}
# Define data and helper functions
marks <- list(
  list(value = 0, label = "0°C"),
  list(value = 20, label = "20°C"),
  list(value = 37, label = "37°C"),
  list(value = 100, label = "100°C")
)

# Create Shiny app
ui <- shinyMaterialUIPage(
  CssBaseline(
    Box(
      sx = list(width = 250),
      Typography(
        id = "track-inverted-slider",
        gutterBottom = TRUE,
        "Inverted track"
      ),
      Slider.shinyInput(
        inputId = "track_inverted_slider",
        track = "inverted",
        `aria-labelledby` = "track-inverted-slider",
        #getAriaValueText = "",
        value = 30,
        marks = marks
      ),
      # Custom separator with theme spacing
      div(style = "height: 24px;"),
      Typography(
        id = "track-inverted-range-slider",
        gutterBottom = TRUE,
        "Inverted track range"
      ),
      Slider.shinyInput(
        inputId = "track_inverted_range_slider",
        track = "inverted",
        `aria-labelledby` = "track-inverted-range-slider",
        #getAriaValueText = "",
        value = c(20, 37),
        marks = marks
      )
    )
  )
)

server <- function(input, output, session) {
  # Empty server since this is a display-only example
}

shinyApp(ui, server)

Non-linear 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 factor 2.

JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Slider from '@mui/material/Slider';

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

function calculateValue(value: number) {
  return 2 ** value;
}

export default function NonLinearSlider() {
  const [value, setValue] = React.useState<number>(10);

  const handleChange = (event: Event, newValue: number) => {
    setValue(newValue);
  };

  return (
    <Box sx={{ width: 250 }}>
      <Typography id="non-linear-slider" gutterBottom>
        Storage: {valueLabelFormat(calculateValue(value))}
      </Typography>
      <Slider
        value={value}
        min={5}
        step={1}
        max={30}
        scale={calculateValue}
        getAriaValueText={valueLabelFormat}
        valueLabelFormat={valueLabelFormat}
        onChange={handleChange}
        valueLabelDisplay="auto"
        aria-labelledby="non-linear-slider"
      />
    </Box>
  );
}
# Helper functions for formatting and scaling
valueLabelFormat <- function(value) {
  units <- c('KB', 'MB', 'GB', 'TB')
  
  unitIndex <- 1
  scaledValue <- value
  
  while (scaledValue >= 1024 && unitIndex < length(units)) {
    unitIndex <- unitIndex + 1
    scaledValue <- scaledValue / 1024
  }
  
  return(paste(round(scaledValue, 2), units[unitIndex]))
}

calculateValue <- function(value) {
  return(2^value)
}

# Create Shiny app
ui <- shinyMaterialUIPage(
  CssBaseline(
    Box(
      sx = list(width = 250, mt = 4),
      textOutput("storage_text"),
      Slider.shinyInput(
        inputId = "non_linear_slider",
        value = 10,
        min = 5,
        step = 1,
        max = 30,
        scale = JS("function(value) { return Math.pow(2, value); }"),
        getAriaValueText = JS("function(value) { 
          const units = ['KB', 'MB', 'GB', 'TB'];
          let unitIndex = 0;
          let scaledValue = Math.pow(2, value);
          
          while (scaledValue >= 1024 && unitIndex < units.length - 1) {
            unitIndex += 1;
            scaledValue /= 1024;
          }
          
          return `${scaledValue.toFixed(2)} ${units[unitIndex]}`;
        }"),
        valueLabelFormat = JS("function(value) { 
          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.toFixed(2)} ${units[unitIndex]}`;
        }"),
        valueLabelDisplay = "auto",
        `aria-labelledby` = "non-linear-slider"
      )
    )
  )
)

server <- function(input, output, session) {
  output$storage_text <- renderText({
    if (!is.null(input$non_linear_slider)) {
      value <- input$non_linear_slider
      calculatedValue <- 2^value
      formattedValue <- valueLabelFormat(calculatedValue)
      paste("Storage:", formattedValue)
    } else {
      "Storage: 1 KB"
    }
  })
}

shinyApp(ui, server)

Accessibility

(WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/)

The component handles most of the work necessary to make it accessible. However, you need to make sure that:

  • Each thumb has a user-friendly label (aria-label, aria-labelledby or getAriaLabel prop).
  • Each thumb has a user-friendly text for its current value. This is not required if the value matches the semantics of the label. You can change the name with the getAriaValueText or aria-valuetext prop.

Unstyled

Use the Base UI Slider for complete ownership of the component’s design, with no Material UI or Joy UI styles to override. This unstyled version of the component is the ideal choice for heavy customization with a smaller bundle size.