Slider
Vue formSingle-thumb slider — pick a value within a range. Optional tick marks, step size, and inline value display.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/slider.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/slider.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/slider.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/slider.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/slider
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
modelValue | ModelValue | — | optional |
defaultValue | ModelValue | — | optional |
range Enable dual-thumb range selection | boolean | — | optional |
vertical Vertical orientation | boolean | — | optional |
height Height when vertical (px or css value) | string | number | — | optional |
marks Tick marks with labels | Record<number, string | SliderMark> | — | optional |
tooltip Show tooltip on drag/focus. Boolean or formatter function | boolean | ((value: number) | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
SliderMark interface SliderMark {
label: string
style?: Record<string, string>
} Dependencies
Used by
Files (2)
-
app/components/ui/slider/Slider.vue 9.2 kB
<script setup lang="ts"> import type { SliderRootProps } from 'reka-ui' import type { HTMLAttributes } from 'vue' import { computed, ref, watch } from 'vue' import { reactiveOmit } from '@vueuse/core' import { SliderRange, SliderRoot, SliderThumb, SliderTrack, TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger, } from 'reka-ui' import { cn } from '@/lib/utils' export interface SliderMark { label: string style?: Record<string, string> } type ModelValue = number | number[] | [number, number] | null | undefined const props = withDefaults( defineProps< Omit<SliderRootProps, 'modelValue' | 'defaultValue' | 'inverted'> & { class?: HTMLAttributes['class'] modelValue?: ModelValue defaultValue?: ModelValue /** Enable dual-thumb range selection */ range?: boolean /** Vertical orientation */ vertical?: boolean /** Height when vertical (px or css value) */ height?: string | number /** Tick marks with labels */ marks?: Record<number, string | SliderMark> /** Show tooltip on drag/focus. Boolean or formatter function */ tooltip?: boolean | ((value: number) => string) /** Show dots at each step */ dots?: boolean /** Reverse direction (right-to-left or bottom-to-top) */ reverse?: boolean /** Highlight the track between thumbs/min (default true) */ included?: boolean /** Size variant */ size?: 'small' | 'default' } >(), { min: 0, max: 100, step: 1, included: true, size: 'default', tooltip: true, }, ) const emits = defineEmits<{ 'update:modelValue': [value: number | number[] | [number, number]] valueCommit: [payload: number | number[] | [number, number]] }>() /* ── normalize value to number[] for Reka UI ── */ const innerValue = ref<number[]>([]) watch( () => props.modelValue, (v) => { const dflt = props.range ? [props.min, props.max] : [props.min] if (v == null) { innerValue.value = dflt return } if (props.range) { const arr = Array.isArray(v) ? v : [v, props.max] innerValue.value = [arr[0] ?? props.min, arr[1] ?? props.max] } else { innerValue.value = Array.isArray(v) ? v : [v] } }, { immediate: true }, ) function handleUpdate(val: number[] | undefined) { if (!val) return innerValue.value = val if (props.range) { emits('update:modelValue', [val[0], val[1]] as [number, number]) } else if (Array.isArray(props.modelValue)) { // backward-compat: consumer passed an array, emit an array emits('update:modelValue', val) } else { emits('update:modelValue', val[0] ?? props.min) } } function handleCommit(val: number[] | undefined) { if (!val) return if (props.range) { emits('valueCommit', [val[0], val[1]] as [number, number]) } else if (Array.isArray(props.modelValue)) { emits('valueCommit', val) } else { emits('valueCommit', val[0] ?? props.min) } } /* ── layout helpers ── */ const orientation = computed(() => { if (props.vertical) return 'vertical' return props.orientation ?? 'horizontal' }) const isHorizontal = computed(() => orientation.value === 'horizontal') const trackSize = computed(() => props.size === 'small' ? (isHorizontal.value ? 'h-1' : 'w-1') : isHorizontal.value ? 'h-1.5' : 'w-1.5', ) const thumbSize = computed(() => (props.size === 'small' ? 'size-3' : 'size-4')) /* ── marks ── */ const markList = computed(() => { if (!props.marks) return [] const entries = Object.entries(props.marks).map(([key, val]) => { const num = Number(key) const label = typeof val === 'string' ? val : val.label const style = typeof val === 'string' ? undefined : val.style const pct = ((num - props.min) / (props.max - props.min)) * 100 return { value: num, label, style, pct } }) entries.sort((a, b) => a.value - b.value) return entries }) /* ── step dots ── */ const dotList = computed(() => { if (!props.dots) return [] const list: number[] = [] const count = Math.floor((props.max - props.min) / props.step) for (let i = 0; i <= count; i++) { list.push(props.min + i * props.step) } return list }) /* ── tooltip ── */ const showTooltip = computed(() => props.tooltip !== false) function formatTooltip(v: number) { if (typeof props.tooltip === 'function') return props.tooltip(v) return String(v) } /* ── delegated reka props ── */ const delegated = reactiveOmit( props, 'class', 'modelValue', 'defaultValue', 'range', 'vertical', 'height', 'marks', 'tooltip', 'dots', 'reverse', 'included', 'size', ) const rootProps = computed(() => ({ ...delegated, modelValue: innerValue.value, orientation: orientation.value, inverted: props.reverse, })) </script> <template> <TooltipProvider> <div :class="cn('relative w-full', !isHorizontal && 'flex flex-col items-center')" :style="!isHorizontal && height ? { height: typeof height === 'number' ? `${height}px` : height } : undefined" > <SliderRoot v-slot="{ modelValue: thumbs }" data-uipkge data-slot="slider" :class=" cn( 'relative flex touch-none select-none data-[disabled]:opacity-50', isHorizontal ? 'w-full items-center' : 'h-full min-h-44 flex-col justify-center', props.class, ) " v-bind="rootProps" @update:model-value="handleUpdate" @value-commit="handleCommit" > <!-- Track --> <SliderTrack data-uipkge data-slot="slider-track" :class="cn('bg-muted relative grow overflow-hidden rounded-full', trackSize)" > <SliderRange v-if="included" data-uipkge data-slot="slider-range" :class="cn('bg-primary absolute', isHorizontal ? 'h-full' : 'w-full')" /> </SliderTrack> <!-- Step dots --> <template v-if="dotList.length"> <div v-for="dot in dotList" :key="dot" :class=" cn( 'border-primary/40 bg-background absolute rounded-full border', isHorizontal ? 'top-1/2 size-1.5 -translate-y-1/2' : 'left-1/2 size-1.5 -translate-x-1/2', props.size === 'small' && 'size-1', ) " :style=" isHorizontal ? { left: `${((dot - min) / (max - min)) * 100}%`, transform: 'translateX(-50%) translateY(-50%)', } : { bottom: `${((dot - min) / (max - min)) * 100}%`, transform: 'translateX(-50%) translateY(50%)', } " /> </template> <!-- Marks --> <div v-if="markList.length" :class=" cn( 'pointer-events-none absolute', isHorizontal ? 'top-full mt-2.5 h-5 w-full' : 'top-0 left-full ml-3 h-full w-20', ) " > <span v-for="mark in markList" :key="mark.value" class="text-muted-foreground absolute text-xs whitespace-nowrap" :style="{ ...mark.style, ...(isHorizontal ? { left: `${mark.pct}%`, transform: 'translateX(-50%)' } : { bottom: `${mark.pct}%`, transform: 'translateY(50%)' }), }" > {{ mark.label }} </span> </div> <!-- Thumbs (with tooltips) --> <template v-for="(_, idx) in thumbs" :key="idx"> <TooltipRoot v-if="showTooltip" :delay-duration="0"> <TooltipTrigger as-child> <SliderThumb :index="idx" data-uipkge data-slot="slider-thumb" :class=" cn( 'border-primary bg-background ring-ring/50 block shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50', thumbSize, ) " /> </TooltipTrigger> <TooltipPortal> <TooltipContent :side="isHorizontal ? 'top' : 'right'" :side-offset="4" class="bg-foreground text-background z-50 w-fit rounded-md px-2 py-1 text-xs" > {{ formatTooltip(thumbs?.[idx] ?? 0) }} <TooltipArrow class="bg-foreground fill-foreground size-2.5 rotate-45 rounded-[2px]" /> </TooltipContent> </TooltipPortal> </TooltipRoot> <SliderThumb v-else :index="idx" data-uipkge data-slot="slider-thumb" :class=" cn( 'border-primary bg-background ring-ring/50 block shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50', thumbSize, ) " /> </template> </SliderRoot> </div> </TooltipProvider> </template> -
app/components/ui/slider/index.ts 0 kB
export { default as Slider } from './Slider.vue'
Raw manifest: https://uipkge.dev/r/vue/slider.json