Slider Allows users to select a value from a continuous range by sliding a handle.
App.svelte tailwind.config.js app.css
< script lang = "ts" >
import { Slider } from "bits-ui" ;
import { cn } from "$lib/utils/styles.js" ;
let value = $ state ( 50 ) ;
</ script >
< div class = "w-full md:max-w-[280px]" >
< Slider . Root
type = "single"
bind : value
class = "relative flex w-full touch-none select-none items-center"
>
{# snippet children () }
< span
class = "relative h-2 w-full grow cursor-pointer overflow-hidden rounded-full bg-dark-10"
>
< Slider . Range class = "absolute h-full bg-foreground" />
</ span >
< Slider . Thumb
index ={ 0 }
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:bg-foreground dark:shadow-card"
) }
/>
{/ 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 : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.04 );
--shadow-mini-inset : 0 px 1 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.04 ) inset ;
--shadow-popover : 0 px 7 px 12 px 3 px hsla ( var ( --dark-10 ));
--shadow-kbd : 0 px 2 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.07 );
--shadow-btn : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.03 );
--shadow-card : 0 px 2 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.04 );
--shadow-date-field-focus : 0 px 0 px 0 px 3 px 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 : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.3 );
--shadow-mini-inset : 0 px 1 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.5 ) inset ;
--shadow-popover : 0 px 7 px 12 px 3 px hsla ( 0 deg 0 % 0 % / 30 % );
--shadow-kbd : 0 px 2 px 0 px 0 px rgba ( 255 , 255 , 255 , 0.07 );
--shadow-btn : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.2 );
--shadow-card : 0 px 2 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.4 );
--shadow-date-field-focus : 0 px 0 px 0 px 3 px 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- [ - 50 px ] mt- [ - 4 px ] ;
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 >
Reusable 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.
MyMultiSlider.svelte
< script lang = "ts" >
import type { ComponentProps } from "svelte" ;
import { Slider } from "bits-ui" ;
type Props = WithoutChildren < ComponentProps < typeof Slider . Root >>;
let { value = $ bindable () , ref = $ bindable ( null ) , ... restProps } : Props = $ props () ;
</ script >
<!--
Since we have to destructure the `value` to make it `$bindable`, we need to use `as any` here to avoid
type errors from the discriminated union of `"single" | "multiple"`.
(an unfortunate consequence of having to destructure bindable values)
-->
< Slider . Root bind : value bind : ref { ... restProps as any } >
{# 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 multiValue = $ state ([ 5 , 10 ]) ;
let singleValue = $ state ( 50 ) ;
</ script >
< MySlider bind : value ={ multiValue } type = "multiple" />
< MySlider bind : value ={ singleValue } type = "single" />
Managing Value State Bits UI offers several approaches to manage and synchronize the Slider's value state, catering to different levels of control and integration needs.
1. Two-Way Binding For seamless state synchronization, use Svelte's bind:value
directive. This method automatically keeps your local state in sync with the component's internal state.
< script lang = "ts" >
import { Slider } from "bits-ui" ;
let myValue = $ state ( 0 ) ;
</ script >
< button onclick ={() => (myValue = 20 ) } > Set value to 20 </ button >
< Slider . Root bind : value ={ myValue } type = "single" >
<!-- ... -->
</ Slider . Root >
Key Benefits Simplifies state management Automatically updates myValue
when the internal state changes (e.g., via dragging the thumb(s)) Allows external control (e.g., updating the value via a separate button) 2. Change Handler For more granular control or to perform additional logic on state changes, use the onValueChange
prop. This approach is useful when you need to execute custom logic alongside state updates.
< script lang = "ts" >
import { Slider } from "bits-ui" ;
let myValue = $ state ( 0 ) ;
</ script >
< Slider . Root
type = "single"
value ={ myValue }
onValueChange ={( v ) => {
myValue = v ;
// additional logic here.
}}
>
<!-- ... -->
</ Slider . Root >
Use Cases Implementing custom behaviors on value change Integrating with external state management solutions Triggering side effects (e.g., logging, data fetching) 3. Fully Controlled For complete control over the component's state, use a Function Binding to manage the value state externally.
< script lang = "ts" >
import { Slider } from "bits-ui" ;
let myValue = $ state ( 0 ) ;
</ script >
< Slider . Root type = "single" bind : value ={() => myValue , ( newValue ) => (myValue = newValue) } >
<!-- ... -->
</ Slider . Root >
When to Use Implementing complex logic Coordinating multiple UI elements Debugging state-related issues Note While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully.
For more in-depth information on controlled components and advanced state management techniques, refer to our Controlled State documentation.
Value Commit You can use the onValueCommit
prop to be notified when the user finishes dragging the thumb and the value changes. This is different than the onValueChange
callback because it waits until the user stops dragging before calling the callback, where the onValueChange
callback is called as the user dragging.
< Slider . Root
onValueCommit ={( v ) => {
console . log ( "user is done sliding!" , v) ;
}}
/>
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" ;
// we have two numbers in the array, so the slider will render two thumbs
let value = $ state ([ 5 , 7 ]) ;
</ script >
< Slider . Root type = "multiple" 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.
Single Type Set the type
prop to "single"
to allow only one accordion item to be open at a time.
< Slider . Root type = "single" />
App.svelte tailwind.config.js app.css
< script lang = "ts" >
import { Slider } from "bits-ui" ;
import { cn } from "$lib/utils/styles.js" ;
let value = $ state ( 50 ) ;
</ script >
< div class = "w-full md:max-w-[280px]" >
< Slider . Root
type = "single"
bind : value
class = "relative flex w-full touch-none select-none items-center"
>
{# snippet children () }
< span
class = "relative h-2 w-full grow cursor-pointer overflow-hidden rounded-full bg-dark-10"
>
< Slider . Range class = "absolute h-full bg-foreground" />
</ span >
< Slider . Thumb
index ={ 0 }
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:bg-foreground dark:shadow-card"
) }
/>
{/ 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 : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.04 );
--shadow-mini-inset : 0 px 1 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.04 ) inset ;
--shadow-popover : 0 px 7 px 12 px 3 px hsla ( var ( --dark-10 ));
--shadow-kbd : 0 px 2 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.07 );
--shadow-btn : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.03 );
--shadow-card : 0 px 2 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.04 );
--shadow-date-field-focus : 0 px 0 px 0 px 3 px 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 : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.3 );
--shadow-mini-inset : 0 px 1 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.5 ) inset ;
--shadow-popover : 0 px 7 px 12 px 3 px hsla ( 0 deg 0 % 0 % / 30 % );
--shadow-kbd : 0 px 2 px 0 px 0 px rgba ( 255 , 255 , 255 , 0.07 );
--shadow-btn : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.2 );
--shadow-card : 0 px 2 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.4 );
--shadow-date-field-focus : 0 px 0 px 0 px 3 px 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- [ - 50 px ] mt- [ - 4 px ] ;
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 ;
}
}
Multiple Type Set the type
prop to "multiple"
to allow multiple accordion items to be open at the same time.
< Slider . Root type = "multiple" />
App.svelte tailwind.config.js app.css
< script lang = "ts" >
import { Slider } from "bits-ui" ;
import { cn } from "$lib/utils/styles.js" ;
let value = $ state ([ 25 , 75 ]) ;
</ script >
< div class = "w-full md:max-w-[280px]" >
< Slider . Root
type = "multiple"
bind : value
class = "relative flex w-full touch-none select-none items-center"
>
{# snippet children ( { thumbs } ) }
< span
class = "relative h-2 w-full grow cursor-pointer 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:bg-foreground dark:shadow-card"
) }
/>
{/ 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 : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.04 );
--shadow-mini-inset : 0 px 1 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.04 ) inset ;
--shadow-popover : 0 px 7 px 12 px 3 px hsla ( var ( --dark-10 ));
--shadow-kbd : 0 px 2 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.07 );
--shadow-btn : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.03 );
--shadow-card : 0 px 2 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.04 );
--shadow-date-field-focus : 0 px 0 px 0 px 3 px 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 : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.3 );
--shadow-mini-inset : 0 px 1 px 0 px 0 px rgba ( 0 , 0 , 0 , 0.5 ) inset ;
--shadow-popover : 0 px 7 px 12 px 3 px hsla ( 0 deg 0 % 0 % / 30 % );
--shadow-kbd : 0 px 2 px 0 px 0 px rgba ( 255 , 255 , 255 , 0.07 );
--shadow-btn : 0 px 1 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.2 );
--shadow-card : 0 px 2 px 0 px 1 px rgba ( 0 , 0 , 0 , 0.4 );
--shadow-date-field-focus : 0 px 0 px 0 px 3 px 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- [ - 50 px ] mt- [ - 4 px ] ;
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 ;
}
}
Vertical Orientation You can use the orientation
prop to change the orientation of the slider, which defaults to "horizontal"
.
< Slider . Root type = "single" 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 type = "single" 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 type = "multiple" autoSort ={ false } >
<!-- ... -->
</ Slider . Root >
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 ]) ;
let desiredIncome = $ state ( 50 ) ;
</ script >
< form method = "POST" >
< MySlider type = "multiple" bind : value ={ expectedIncome } />
< input type = "hidden" name = "expectedIncomeStart" value ={ expectedIncome[ 0 ] } />
< input type = "hidden" name = "expectedIncomeEnd" value ={ expectedIncome[ 1 ] } />
< MySlider type = "single" bind : value ={ desiredIncome } />
< input type = "hidden" name = "expectedIncomeEnd" value ={ desiredIncome } />
< button type = "submit" > Submit </ button >
</ form >
API Reference
Slider. Root The root slider component which contains the remaining slider components.
Property Type Description type
required union
See type definition The type of the slider. If set to 'multiple'
, the slider will allow multiple thumbs and the value
will be an array of numbers.
Default: —— undefined
value
$bindable number
The current value of the slider. If the type
is set to 'multiple'
, this should be an array of numbers and will default to an empty array.
Default: 0
onValueChange
function
See type definition A callback function called when the value state of the slider changes.
Default: —— undefined
onValueCommit
function
See type definition A callback function called when the user finishes dragging the thumb and the value changes. This is different than the onValueChange
callback because it waits until the user stops dragging before calling the callback, where the onValueChange
callback is called immediately after the user starts dragging.
Default: —— undefined
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
The orientation of the slider.
Default: 'horizontal'
step
number
The step value of the slider.
Default: 1
dir
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. This is only applicable to the 'multiple'
type.
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
See type definition Use render delegation to render your own element. See Child Snippet docs for more information.
Default: —— undefined
Data Attribute Value Description data-orientation
The orientation of the slider.
data-slider-root
''
Present on the root element.
Slider. Range 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
See type definition Use render delegation to render your own element. See Child Snippet docs for more information.
Default: —— undefined
Data Attribute Value Description data-slider-range
''
Present on the range elements.
Slider. Thumb A thumb on the slider.
Property Type Description index
required number
The index of the thumb in the array of thumbs provided by the thumbs
children
snippet prop.
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
See type definition Use render delegation to render your own element. See Child Snippet docs for more information.
Default: —— undefined
Data Attribute Value Description data-slider-thumb
''
Present on the thumb elements.
Slider. Tick 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 ticks
children
snippet prop.
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
See type definition Use render delegation to render your own element. See Child Snippet 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.