Slider
Allows users to select a value from a continuous range by sliding a handle.
<script lang="ts">
import { Slider } from "bits-ui";
import { cn } from "$lib/utils/styles.js";
let value = [20, 80];
</script>
<div class="w-full md:max-w-[280px]">
<Slider.Root
step={25}
bind:value
class="relative flex w-full touch-none select-none items-center"
>
{#snippet children({ thumbs, ticks })}
<span
class="relative h-2 w-full grow overflow-hidden rounded-full bg-dark-10"
>
<Slider.Range class="absolute h-full bg-foreground" />
</span>
{#each thumbs as index}
<Slider.Thumb
{index}
class={cn(
"block size-[25px] cursor-pointer rounded-full border border-border-input bg-background shadow transition-colors hover:border-dark-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 active:scale-98 disabled:pointer-events-none disabled:opacity-50 dark:shadow-card",
index === 0 ? "bg-blue-400" : "bg-yellow-300"
)}
/>
{/each}
{#each ticks as index}
<Slider.Tick {index} class="block h-1 w-1 bg-dark-10" />
{/each}
{/snippet}
</Slider.Root>
</div>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Structure
<script lang="ts">
import { Slider } from "bits-ui";
</script>
<Slider.Root>
<Slider.Range />
<Slider.Thumb />
<Slider.Tick />
</Slider.Root>
Resuable Components
Bits UI provides primitives that enable you to build your own custom slider component that can be reused throughout your application.
Here's an example of how you might create a reusable MySlider
component.
<script lang="ts">
import { Slider } from "bits-ui";
type Props = WithoutChildren<Slider.RootProps>;
let { value = $bindable(), ref = $bindable(null), ...restProps }: Props = $props();
</script>
<Slider.Root bind:value bind:ref {...restProps}>
{#snippet children({ thumbs, ticks })}
<Slider.Range />
{#each thumbs as index}
<Slider.Thumb {index} />
{/each}
{#each ticks as index}
<Slider.Tick {index} />
{/each}
{/snippet}
</Slider.Root>
You can then use the MySlider
component in your application like so:
<script lang="ts">
import MySlider from "$lib/components/MySlider.svelte";
let someValue = $state([5, 10]);
</script>
<MySlider bind:value={someValue} />
Value State
The value
represents the currently selected value(s) of the slider.
Two-Way Binding
Use the bind:value
directive for effortless two-way synchronization between your local state and the slider's internal state.
<script lang="ts">
import { Slider } from "bits-ui";
let value = $state([5, 7]);
</script>
<Slider.Root bind:value>
<!-- ... -->
</Slider.Root>
Change Handler
You can also use the onValueChange
prop to update local state when the slider's value changes.
<Slider.Root onValueChange={(value) => console.log(value)}>
<!-- ... -->
</Slider.Root>
Change End Handler
Sometimes, you may only want to perform an action or update a state when the user has finished dragging the thumb, but not as they are dragging it. You can use the onValueChangeEnd
prop to listen for the end of the value change.
<Slider.Root onValueChangeEnd={() => console.log("value changed!")}>
<!-- ... -->
</Slider.Root>
Controlled
Sometimes, you may want complete control over the component's value
state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the controlledValue
prop to true
.
You will then be responsible for updating a local value state variable that is passed as the value
prop to the Slider.Root
component.
<script lang="ts">
import { Slider } from "bits-ui";
let myValue = $state([]);
</script>
<Slider.Root controlledValue value={myValue} onValueChange={(v) => (myValue = v)}>
<!-- ... -->
</Slider.Root>
See the Controlled State documentation for more information about controlled states.
Multiple Thumbs and Ticks
If the value
prop has more than one value, the slider will render multiple thumbs. You can also use the ticks
snippet prop to render ticks at specific intervals
<script lang="ts">
import { Slider } from "bits-ui";
let value = $state([5, 7]);
</script>
<Slider.Root min={0} max={10} step={1} bind:value>
{#snippet children({ ticks, thumbs })}
<Slider.Range />
{#each thumbs as index}
<Slider.Thumb {index} />
{/each}
{#each ticks as index}
<Slider.Tick {index} />
{/each}
{/snippet}
</Slider.Root>
To determine the number of ticks that will be rendered, you can simply divide the max
value by the step
value.
Vertical Orientation
You can use the orientation
prop to change the orientation of the slider, which defaults to "horizontal"
.
<Slider.Root orientation="vertical">
<!-- ... -->
</Slider.Root>
RTL Support
You can use the dir
prop to change the reading direction of the slider, which defaults to "ltr"
.
<Slider.Root dir="rtl">
<!-- ... -->
</Slider.Root>
Auto Sort
By default, the slider will sort the values from smallest to largest, so if you drag a smaller thumb to a larger value, the value of that thumb will be updated to the larger value.
You can disable this behavior by setting the autoSort
prop to false
.
<Slider.Root autoSort={false}>
<!-- ... -->
</Slider.Root>
HTML Forms
Since there is a near endless number of possible values that a user can select, the slider does not render a hidden input element by default.
You'll need to determine how you want to submit the value(s) of the slider with a form.
Here's an example of how you might do that:
<script lang="ts">
import MySlider from "$lib/components/MySlider.svelte";
let expectedIncome = $state([50, 100]);
</script>
<form method="POST">
<MySlider bind:value={expectedIncome} />
<input type="hidden" name="expectedIncomeStart" value={expectedIncome[0]} />
<input type="hidden" name="expectedIncomeEnd" value={expectedIncome[1]} />
<button type="submit">Submit</button>
</form>
API Reference
The root slider component which contains the remaining slider components.
Property | Type | Description |
---|---|---|
value $bindable | number[] | The current value of the slider. Default: [] |
onValueChange | function | A callback function called when the value state of the slider changes. Default: undefined |
onValueChangeEnd | function | A callback function called when the user finishes dragging the thumb and the value changes. This is different than the Default: undefined |
controlledValue | boolean | Whether or not the value is controlled or not. If Default: false |
disabled | boolean | Whether or not the switch is disabled. Default: false |
max | number | The maximum value of the slider. Default: 100 |
min | number | The minimum value of the slider. Default: 0 |
orientation | enum | The orientation of the slider. Default: "horizontal" |
step | number | The step value of the slider. Default: 1 |
dir | enum | The reading direction of the app. Default: ltr |
autoSort | boolean | Whether to automatically sort the values in the array when moving thumbs past one another. Default: true |
ref $bindable | HTMLSpanElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-orientation | enum | The orientation of the slider. |
data-slider-root | '' | Present on the root element. |
The range of the slider.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLSpanElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-slider-range | '' | Present on the range elements. |
A thumb on the slider.
Property | Type | Description |
---|---|---|
index required | number | The index of the thumb in the array of thumbs provided by the Default: undefined |
disabled | boolean | Whether or not the thumb is disabled. Default: false |
ref $bindable | HTMLSpanElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-slider-thumb | '' | Present on the thumb elements. |
A tick mark on the slider.
Property | Type | Description |
---|---|---|
index required | number | The index of the tick in the array of ticks provided by the Default: undefined |
ref $bindable | HTMLSpanElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-bounded | '' | Present when the tick is bounded. |
data-slider-tick | '' | Present on the tick elements. |