Animated Slider

Slider Tag Wrapper with shadcn slider and numberflow.

Width

Dual Range

Position

Static

70

Installation

npm install @number-flow/react @radix-ui/react-slider
slider.tsx
1
'use client';
2
3
import * as React from 'react';
4
import * as SliderPrimitive from '@radix-ui/react-slider';
5
import { cn } from '@/lib/utils';
6
import NumberFlow from '@number-flow/react';
7
8
interface DualRangeSliderProps
9
extends React.ComponentProps<typeof SliderPrimitive.Root> {
10
labelPosition?: 'top' | 'bottom' | 'static';
11
lableContenPos?: 'left' | 'right';
12
label?: React.ReactNode | ((value: number | undefined) => React.ReactNode);
13
}
14
15
const DualRangeSlider = React.forwardRef<
16
React.ElementRef<typeof SliderPrimitive.Root>,
17
DualRangeSliderProps
18
>(
19
(
20
{
21
className,
22
label,
23
labelPosition = 'top',
24
lableContenPos = 'right',
25
...props
26
},
27
ref
28
) => {
29
const initialValue = Array.isArray(props.value)
30
? props.value
31
: [props.min, props.max];
32
33
return (
34
<SliderPrimitive.Root
35
ref={ref}
36
className={cn(
37
'relative flex w-full touch-none select-none items-center',
38
className
39
)}
40
{...props}
41
>
42
<SliderPrimitive.Track className='relative h-2 w-full grow overflow-hidden rounded-full dark:bg-gray-800 bg-gray-300'>
43
<SliderPrimitive.Range className='absolute h-full bg-primary' />
44
</SliderPrimitive.Track>
45
<>
46
{initialValue.map((value, index) => (
47
<React.Fragment key={index}>
48
<SliderPrimitive.Thumb className='relative block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'>
49
{label && labelPosition !== 'static' && (
50
<div
51
className={cn(
52
'absolute flex w-full justify-center items-start gap-0.5',
53
labelPosition === 'top' && '-top-7',
54
labelPosition === 'bottom' && 'top-4'
55
)}
56
>
57
{lableContenPos === 'left' && (
58
<>
59
{typeof label === 'function' ? (
60
<span className='inline-block -translate-y-0.5'>
61
{label(value)}
62
</span>
63
) : (
64
label && (
65
<span className='inline-block '>{label}</span>
66
)
67
)}
68
</>
69
)}
70
<NumberFlow
71
willChange
72
// @ts-ignore
73
value={value}
74
isolate
75
opacityTiming={{
76
duration: 250,
77
easing: 'ease-out',
78
}}
79
transformTiming={{
80
easing: `linear(0, 0.0033 0.8%, 0.0263 2.39%, 0.0896 4.77%, 0.4676 15.12%, 0.5688, 0.6553, 0.7274, 0.7862, 0.8336 31.04%, 0.8793, 0.9132 38.99%, 0.9421 43.77%, 0.9642 49.34%, 0.9796 55.71%, 0.9893 62.87%, 0.9952 71.62%, 0.9983 82.76%, 0.9996 99.47%)`,
81
duration: 500,
82
}}
83
/>
84
{lableContenPos === 'right' && (
85
<>
86
{typeof label === 'function' ? (
87
<span className='inline-block -translate-y-1'>
88
{label(value)}
89
</span>
90
) : (
91
label && (
92
<span className='inline-block '>{label}</span>
93
)
94
)}
95
</>
96
)}
97
</div>
98
)}
99
</SliderPrimitive.Thumb>
100
</React.Fragment>
101
))}
102
</>
103
104
{label && labelPosition === 'static' && (
105
<>
106
{initialValue.map((value, index) => (
107
<div
108
className={cn(
109
'absolute -top-7 w-fit right-0 flex justify-center items-start gap-0.5'
110
)}
111
>
112
{lableContenPos === 'left' && (
113
<>
114
{typeof label === 'function' ? (
115
<span className='inline-block -translate-y-0.5'>
116
{label(value)}
117
</span>
118
) : (
119
label && <span className='inline-block '>{label}</span>
120
)}
121
</>
122
)}
123
<NumberFlow
124
willChange
125
// @ts-ignore
126
value={value}
127
isolate
128
opacityTiming={{
129
duration: 250,
130
easing: 'ease-out',
131
}}
132
transformTiming={{
133
easing: `linear(0, 0.0033 0.8%, 0.0263 2.39%, 0.0896 4.77%, 0.4676 15.12%, 0.5688, 0.6553, 0.7274, 0.7862, 0.8336 31.04%, 0.8793, 0.9132 38.99%, 0.9421 43.77%, 0.9642 49.34%, 0.9796 55.71%, 0.9893 62.87%, 0.9952 71.62%, 0.9983 82.76%, 0.9996 99.47%)`,
134
duration: 500,
135
}}
136
/>
137
{lableContenPos === 'right' && (
138
<>
139
{typeof label === 'function' ? (
140
<span className='inline-block -translate-y-1'>
141
{label(value)}
142
</span>
143
) : (
144
label && <span className='inline-block '>{label}</span>
145
)}
146
</>
147
)}
148
</div>
149
))}
150
</>
151
)}
152
</SliderPrimitive.Root>
153
);
154
}
155
);
156
DualRangeSlider.displayName = 'DualRangeSlider';
157
158
export { DualRangeSlider };

Props

PropTypeDefaultDescription
labelReact.ReactNode or (value?: number) => ReactNodeundefinedThe content for the label, either as a node or a function receiving the current value.
labelPosition'top' | 'bottom' | 'static''top'The position of the label relative to the slider thumb.
lableContenPos'left' | 'right''right'The position of the label content within the label block.
classNamestringundefinedAdditional class name(s) for the outer container of the slider.
valuenumber[]undefinedThe current value(s) of the slider. Can be a single value or a range (array).
minnumberundefinedThe minimum value allowed for the slider.
maxnumberundefinedThe maximum value allowed for the slider.
stepnumberundefinedThe step increment for the slider values.
refReact.RefObjectundefinedA React ref object for the slider's root element.

Inspire from shadcn/ui expansions