Skip to content
edmi-ui

Slider

Pick a value or a range by dragging. Array value: 1 = single, 2 = range, 3+ = multiple thumbs.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/slider
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/slider
Terminal window
yarn dlx shadcn@latest add @edmi-ui/slider
Terminal window
bunx shadcn@latest add @edmi-ui/slider

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/slider
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/slider
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/slider
Terminal window
bunx shadcn-vue@latest add @edmi-ui/slider

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/slider.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/slider.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/slider.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/slider.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Slider } from "@/components/ui/slider";
<div className="flex w-72 flex-col gap-6">
<Slider defaultValue={[33]} max={100} step={1} />
<Slider defaultValue={[25, 75]} />
<Slider defaultValue={[20, 50, 80]} />
</div>
<script setup lang="ts">
import { Slider } from "@/components/ui/slider";
</script>
<template>
<div class="flex w-72 flex-col gap-6">
<Slider :default-value="[33]" :max="100" :step="1" />
<Slider :default-value="[25, 75]" />
<Slider :default-value="[20, 50, 80]" />
</div>
</template>
<script lang="ts">
import { Slider } from "$lib/components/ui/slider";
</script>
<div class="flex w-72 flex-col gap-6">
<Slider type="multiple" value={[33]} max={100} step={1} />
<Slider type="multiple" value={[25, 75]} />
<Slider type="multiple" value={[20, 50, 80]} />
</div>
registry/ui/slider.tsx
"use client";
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
import { cn } from "cn";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// Array value: 1 = single, 2 = range, 3+ = multiple thumbs.
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
elevation,
...props
}: SliderPrimitive.Root.Props & {
/** ✦ depth: raised +1 / floating +2 make the thumbs rise (never the track). */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "handle");
const raised = level === "raised" || level === "floating";
const _values = Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min];
return (
<SliderPrimitive.Root
className={cn(
"data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full",
className,
)}
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
thumbAlignment="edge"
{...props}
>
<SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col">
<SliderPrimitive.Track
data-slot="slider-track"
className="relative grow overflow-hidden rounded-full border border-border bg-muted select-none data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
>
<SliderPrimitive.Indicator
data-slot="slider-range"
className="rounded-full bg-brand select-none data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
// biome-ignore lint/suspicious/noArrayIndexKey: thumbs are positional
key={index}
index={index}
data-slot="slider-thumb"
className={cn(
"relative block size-[18px] shrink-0 rounded-full border border-brand-edge bg-white transition-shadow outline-none select-none after:absolute after:-inset-2 focus-visible:shadow-[0_0_0_4px_var(--ring-soft)] data-[dragging]:shadow-[0_0_0_4px_var(--ring-soft)]",
raised &&
"border-transparent bg-linear-to-b from-white to-[#eeede9] shadow-thumb focus-visible:shadow-[0_0_0_4px_var(--ring-soft),0_0_1.5px_rgb(0_0_0/0.45)] data-[dragging]:shadow-[0_0_0_4px_var(--ring-soft),0_0_1.5px_rgb(0_0_0/0.45)]",
)}
/>
))}
</SliderPrimitive.Control>
</SliderPrimitive.Root>
);
}
export { Slider };
registry/ui/slider/Slider.vue
<script setup lang="ts">
import type { SliderRootEmits, SliderRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { SliderRange, SliderRoot, SliderThumb, SliderTrack, useForwardPropsEmits } from "reka-ui"
import { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
// Array value: 1 = single, 2 = range, 3+ = multiple thumbs.
const props = withDefaults(defineProps<SliderRootProps & { class?: HTMLAttributes["class"], elevation?: Elevation }>(), { elevation: undefined })
const emits = defineEmits<SliderRootEmits>()
const delegatedProps = reactiveOmit(props, "class", "elevation")
const level = useElevation(() => props.elevation, "handle")
const raised = computed(() => level.value === "raised" || level.value === "floating")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<SliderRoot
v-slot="{ modelValue }"
data-slot="slider"
:class="
cn(
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
props.class,
)
"
v-bind="forwarded"
>
<SliderTrack
data-slot="slider-track"
class="relative grow overflow-hidden rounded-full border border-border bg-muted select-none data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
>
<SliderRange
data-slot="slider-range"
class="absolute rounded-full bg-brand select-none data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
/>
</SliderTrack>
<SliderThumb
v-for="(_, key) in modelValue"
:key="key"
data-slot="slider-thumb"
:class="cn(
'relative block size-[18px] shrink-0 rounded-full border border-brand-edge bg-white transition-shadow outline-none select-none after:absolute after:-inset-2 focus-visible:shadow-[0_0_0_4px_var(--ring-soft)] data-[dragging]:shadow-[0_0_0_4px_var(--ring-soft)] active:shadow-[0_0_0_4px_var(--ring-soft)] disabled:pointer-events-none',
raised && 'border-transparent bg-linear-to-b from-white to-[#eeede9] shadow-thumb focus-visible:shadow-[0_0_0_4px_var(--ring-soft),0_0_1.5px_rgb(0_0_0/0.45)] data-[dragging]:shadow-[0_0_0_4px_var(--ring-soft),0_0_1.5px_rgb(0_0_0/0.45)] active:shadow-[0_0_0_4px_var(--ring-soft),0_0_1.5px_rgb(0_0_0/0.45)]',
)"
/>
</SliderRoot>
</template>
registry/ui/slider/index.ts
export { default as Slider } from "./Slider.vue";
src/lib/registry/ui/slider/slider.svelte
<script lang="ts">
import { Slider as SliderPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
// Array value: 1 = single, 2 = range, 3+ = multiple thumbs.
let {
ref = $bindable(null),
value = $bindable(),
orientation = "horizontal",
class: className,
elevation = "auto",
...restProps
}: WithoutChildrenOrChild<SliderPrimitive.RootProps> & {
/** ✦ depth: raised +1 / floating +2 make the thumbs rise (never the track). */
elevation?: Elevation;
} = $props();
const level = useElevation(() => elevation, "handle");
const raised = $derived(level.current === "raised" || level.current === "floating");
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<SliderPrimitive.Root
bind:ref
bind:value={value as never}
data-slot="slider"
{orientation}
class={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...restProps}
>
{#snippet children({ thumbItems })}
<span
data-slot="slider-track"
data-orientation={orientation}
class="relative grow overflow-hidden rounded-full border border-border bg-muted select-none data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
>
<SliderPrimitive.Range
data-slot="slider-range"
class="absolute rounded-full bg-brand select-none data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
/>
</span>
{#each thumbItems as thumb (thumb.index)}
<SliderPrimitive.Thumb
data-slot="slider-thumb"
index={thumb.index}
class={cn(
"relative block size-[18px] shrink-0 rounded-full border border-brand-edge bg-white transition-shadow outline-none select-none after:absolute after:-inset-2 focus-visible:shadow-[0_0_0_4px_var(--ring-soft)] data-[active]:shadow-[0_0_0_4px_var(--ring-soft)] disabled:pointer-events-none",
raised &&
"border-transparent bg-linear-to-b from-white to-[#eeede9] shadow-thumb focus-visible:shadow-[0_0_0_4px_var(--ring-soft),0_0_1.5px_rgb(0_0_0/0.45)] data-[active]:shadow-[0_0_0_4px_var(--ring-soft),0_0_1.5px_rgb(0_0_0/0.45)]"
)}
/>
{/each}
{/snippet}
</SliderPrimitive.Root>
src/lib/registry/ui/slider/index.ts
import Root from "./slider.svelte";
export {
Root,
//
Root as Slider,
};

See the Elevation guide for the levels, roles and layered mode.

Only the thumbs rise (raised +1 or floating +2); the track and range never get a bevel.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Raised (+1)

Flat (0)

Raised (+1)

Flat (0)

Raised (+1)

Built on Base UI Slider.

Prop Type Default
value / defaultValue number[] -
min / max / step number 0 / 100 / 1
orientation "horizontal" | "vertical" "horizontal"
onValueChange (value: number[], event) => void -
disabled boolean false
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
  • Brand-colored range and a round thumb (elevation raises the thumbs); focus shows a soft ring.