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 <- muiMaterialPage(
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 <- muiMaterialPage(
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)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 <- muiMaterialPage(
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 <- muiMaterialPage(
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't win)</b>
</Typography>
<Typography noWrap sx={{ letterSpacing: -0.25 }}>
Chilling Sunday — คนเก่าเขาทำไว้ดี
</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 <- muiMaterialPage(
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:
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 <- muiMaterialPage(
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 <- muiMaterialPage(
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 <- muiMaterialPage(
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 <- muiMaterialPage(
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.
