Closed Sadik-Hossain closed 7 months ago
shadcn-ui is based on radix-ui and already allows multiple values to be selected.
https://www.radix-ui.com/docs/primitives/components/slider#create-a-range
I wasn't successful trying to implement the range features provided by radix and took a roundabout way to enhance the component that shadcn-ui provides. This updated component below adds label support and range support. Updated component
import React, { useState } from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '@/components/app/shadcn/lib/utils';
const Slider = React.forwardRef(({ className, min, max, step, formatLabel, value, onValueChange, ...props }, ref) => {
const initialValue = Array.isArray(value) ? value : [min, max];
const [localValues, setLocalValues] = useState(initialValue);
const handleValueChange = (newValues) => {
setLocalValues(newValues);
if (onValueChange) {
onValueChange(newValues);
}
};
return (
<SliderPrimitive.Root
ref={ref}
min={min}
max={max}
step={step}
value={localValues}
onValueChange={handleValueChange}
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
{localValues.map((value, index) => (
<React.Fragment key={index}>
<div className="absolute text-center" style={{ left: `calc(${((value - min) / (max - min)) * 100}% + 0px)` , top: `10px`}}>
<span className="text-xs">{formatLabel ? formatLabel(value) : value}</span>
</div>
<SliderPrimitive.Thumb
className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
/>
</React.Fragment>
))}
</SliderPrimitive.Root>
);
});
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
Example implementation:
import { Slider } from "./path-to-enhanced-slider"; // Adjust the import path accordingly
const MyComponent = () => {
const [range, setRange] = useState([0, 24]);
const handleRangeChange = (value) => {
setRange(value);
};
return (
<Slider
defaultValue={[0, 24]}
max={48}
min={0}
step={1}
value={range}
onValueChange={handleRangeChange}
formatLabel={(value) => `${value} hrs`}
/>
);
};```
NextJS/Typescript version of @atwellpub's code
import React, { useState } from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
type SliderProps = {
className?: string;
min: number;
max: number;
minStepsBetweenThumbs: number;
step: number;
formatLabel?: (value: number) => string;
value?: number[] | readonly number[];
onValueChange?: (values: number[]) => void;
};
const Slider = React.forwardRef(
(
{
className,
min,
max,
step,
formatLabel,
value,
onValueChange,
...props
}: SliderProps,
ref
) => {
const initialValue = Array.isArray(value) ? value : [min, max];
const [localValues, setLocalValues] = useState(initialValue);
const handleValueChange = (newValues: number[]) => {
setLocalValues(newValues);
if (onValueChange) {
onValueChange(newValues);
}
};
return (
<SliderPrimitive.Root
ref={ref as React.RefObject<HTMLDivElement>}
min={min}
max={max}
step={step}
value={localValues}
onValueChange={handleValueChange}
className={cn(
"relative flex w-full touch-none select-none mb-6 items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
{localValues.map((value, index) => (
<React.Fragment key={index}>
<div
className="absolute text-center"
style={{
left: `calc(${((value - min) / (max - min)) * 100}% + 0px)`,
top: `10px`,
}}
>
<span className="text-sm">
{formatLabel ? formatLabel(value) : value}
</span>
</div>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</React.Fragment>
))}
</SliderPrimitive.Root>
);
}
);
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
Root classes : "relative flex w-full touch-none select-none items-center" Track classes : "relative h-2 w-full grow overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800" Range classes : "absolute h-full bg-slate-900 dark:bg-slate-50" Thumb Classes : "block h-5 w-5 rounded-full border-2 border-slate-900 bg-white ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-50 dark:bg-slate-950 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300"
And these are the classes for the default styles that comes from shadcn. Add them to the @duncanmcdowell's TS solution.
Hello,
Thanks for your solutions. I was struggling resetting the value of the slider with a button, as the thumbs were not translating the updated value passed through the props. I added a useEffect in the solution from @duncanmcdowell and @atwellpub (thanks to both) in order to perform this update:
// NextJS/Typescript version of @atwellpub's code
import React, { useState } from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
type SliderProps = {
className?: string;
min: number;
max: number;
minStepsBetweenThumbs: number;
step: number;
formatLabel?: (value: number) => string;
value?: number[] | readonly number[];
onValueChange?: (values: number[]) => void;
};
const Slider = React.forwardRef(
(
{
className,
min,
max,
step,
formatLabel,
value,
onValueChange,
...props
}: SliderProps,
ref
) => {
const initialValue = Array.isArray(value) ? value : [min, max];
const [localValues, setLocalValues] = useState(initialValue);
useEffect(() => {
// Update localValues when the external value prop changes
setLocalValues(Array.isArray(value) ? value : [min, max]);
}, [min, max, value]);
const handleValueChange = (newValues: number[]) => {
setLocalValues(newValues);
if (onValueChange) {
onValueChange(newValues);
}
};
return (
<SliderPrimitive.Root
ref={ref as React.RefObject<HTMLDivElement>}
min={min}
max={max}
step={step}
value={localValues}
onValueChange={handleValueChange}
className={cn(
"relative flex w-full touch-none select-none mb-6 items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
{localValues.map((value, index) => (
<React.Fragment key={index}>
<div
className="absolute text-center"
style={{
left: `calc(${((value - min) / (max - min)) * 100}% + 0px)`,
top: `10px`,
}}
>
<span className="text-sm">
{formatLabel ? formatLabel(value) : value}
</span>
</div>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</React.Fragment>
))}
</SliderPrimitive.Root>
);
}
);
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
Hello,
Thanks for your solutions. I was struggling resetting the value of the slider with a button, as the thumbs were not translating the updated value passed through the props. I added a useEffect in the solution from @duncanmcdowell and @atwellpub (thanks to both) in order to perform this update:
// NextJS/Typescript version of @atwellpub's code import React, { useState } from "react"; import * as SliderPrimitive from "@radix-ui/react-slider"; import { cn } from "@/lib/utils"; type SliderProps = { className?: string; min: number; max: number; minStepsBetweenThumbs: number; step: number; formatLabel?: (value: number) => string; value?: number[] | readonly number[]; onValueChange?: (values: number[]) => void; }; const Slider = React.forwardRef( ( { className, min, max, step, formatLabel, value, onValueChange, ...props }: SliderProps, ref ) => { const initialValue = Array.isArray(value) ? value : [min, max]; const [localValues, setLocalValues] = useState(initialValue); useEffect(() => { // Update localValues when the external value prop changes setLocalValues(Array.isArray(value) ? value : [min, max]); }, [min, max, value]); const handleValueChange = (newValues: number[]) => { setLocalValues(newValues); if (onValueChange) { onValueChange(newValues); } }; return ( <SliderPrimitive.Root ref={ref as React.RefObject<HTMLDivElement>} min={min} max={max} step={step} value={localValues} onValueChange={handleValueChange} className={cn( "relative flex w-full touch-none select-none mb-6 items-center", className )} {...props} > <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20"> <SliderPrimitive.Range className="absolute h-full bg-primary" /> </SliderPrimitive.Track> {localValues.map((value, index) => ( <React.Fragment key={index}> <div className="absolute text-center" style={{ left: `calc(${((value - min) / (max - min)) * 100}% + 0px)`, top: `10px`, }} > <span className="text-sm"> {formatLabel ? formatLabel(value) : value} </span> </div> <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" /> </React.Fragment> ))} </SliderPrimitive.Root> ); } ); Slider.displayName = SliderPrimitive.Root.displayName; export { Slider };
Added the label inside the thumb to follow the thumb without calc
// NextJS/Typescript version of @atwellpub's code
import { Fragment, useEffect, useState, forwardRef } from 'react';
import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '../../../utils/utils';
export type SliderProps = {
className?: string;
min: number;
max: number;
minStepsBetweenThumbs: number;
step: number;
formatLabel?: (value: number) => string;
value?: number[] | readonly number[];
onValueChange?: (values: number[]) => void;
};
const Slider = forwardRef(
({ className, min, max, step, formatLabel, value, onValueChange, ...props }: SliderProps, ref) => {
const initialValue = Array.isArray(value) ? value : [min, max];
const [localValues, setLocalValues] = useState(initialValue);
useEffect(() => {
// Update localValues when the external value prop changes
setLocalValues(Array.isArray(value) ? value : [min, max]);
}, [min, max, value]);
const handleValueChange = (newValues: number[]) => {
setLocalValues(newValues);
if (onValueChange) {
onValueChange(newValues);
}
};
return (
<SliderPrimitive.Root
ref={ref as React.RefObject<HTMLDivElement>}
min={min}
max={max}
step={step}
value={localValues}
onValueChange={handleValueChange}
className={cn('relative flex w-full touch-none select-none mb-6 items-center', className)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
{localValues.map((value, index) => (
<Fragment key={index}>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50">
<div className="absolute -top-8 left-1/2 transform -translate-x-1/2 z-30 rounded-md border bg-popover text-popover-foreground shadow-sm px-2">
{formatLabel ? formatLabel(value) : value}
</div>
</SliderPrimitive.Thumb>
</Fragment>
))}
</SliderPrimitive.Root>
);
}
);
Slider.displayName = SliderPrimitive.Root.displayName;
export { Slider };
Guys, you can just simplify by using Slider props, do not need recreate props.
Slider Component:
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "#/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => {
const initialValue = Array.isArray(props.value) ? props.value : [props.min, props.max]
return (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
{initialValue.map((value, index) => (
<React.Fragment key={index}>
<SliderPrimitive.Thumb className="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" />
</React.Fragment>
))}
</SliderPrimitive.Root>
)
})
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
Usage component:
"use client"
export function PriceControl() {
const [localValues, setLocalValues] = useState([0, 100_000])
const handleValueChange = (newValues: any) => {
setLocalValues(newValues)
}
return (
<div className="grid gap-4 p-4 w-full max-w-80 bg-white border border-[#14424C]/20 rounded-[12px]">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
Preço
</label>
<Slider
defaultValue={defaultValue}
minStepsBetweenThumbs={10_000}
max={300_000}
min={0}
step={1}
onValueChange={handleValueChange}
className={cn("w-full")}
/>
<div className="flex gap-2 flex-wrap">
<ol className="flex items-center w-full gap-3">
{localValues.map((_, index) => (
<li key={index} className="flex items-center justify-between w-full border px-3 h-10 rounded-md">
<span>Km</span>
<span>{localValues[index]}</span>
</li>
))}
</ol>
</div>
</div>
)
}
I hope this helps, and if someone want try to discover why doesn't work with another step value, please provide your solution :)
Honestly this should be how the component actually works. Took me way too long to find this answer. @dracoalv any chance you intend to make a PR out of this? If not, I'd be happy to facilitate it.
Currently, the Shadcn Library provides a range slider component that allows users to select a single value within a given range. However, there are scenarios where having a dual range, multi-range slider would be highly beneficial. This type of slider would enable users to select two values, defining a range between them.
A dual range multirange slider would allow developers to handle more complex data selection scenarios. It would enable users to define a range instead of a single value, which is particularly useful in applications such as data filtering, date range selection, and price range selection