Slider
A range slider component with blocky styling.
The Slider component allows users to select a value from a range by dragging a handle. It features bold borders and shadows that match the Things design system. Built from scratch using React and native HTML input elements. No dependencies on any UI library.
Code
TypeScript: Copy this code into components/ui/slider.tsx:
tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
export interface SliderProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
value?: number[]
defaultValue?: number[]
onValueChange?: (value: number[]) => void
}
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
({ className, value, defaultValue, onValueChange, min = 0, max = 100, step = 1, disabled, ...props }, ref) => {
const [internalValue, setInternalValue] = React.useState<number>(
value?.[0] ?? defaultValue?.[0] ?? Number(min)
)
React.useEffect(() => {
if (value !== undefined && value[0] !== undefined) {
setInternalValue(value[0])
}
}, [value])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) return
const newValue = Number(e.target.value)
setInternalValue(newValue)
onValueChange?.([newValue])
}
const percentage = ((internalValue - Number(min)) / (Number(max) - Number(min))) * 100
return (
<div className={cn("relative flex w-full items-center", className)}>
<input
type="range"
min={min}
max={max}
step={step}
value={internalValue}
onChange={handleChange}
disabled={disabled}
ref={ref}
className="sr-only"
{...props}
/>
<div className="relative h-2 w-full rounded-full border-2 border-foreground bg-muted neobrutalism-shadow-sm">
<div
className={cn(
"absolute h-full rounded-full border-2 border-foreground bg-primary transition-all neobrutalism-shadow-sm",
disabled && "opacity-50"
)}
style={{ width: `${percentage}%` }}
/>
</div>
<div
className={cn(
"absolute h-5 w-5 -translate-x-1/2 transform rounded-sm border-2 border-foreground bg-background transition-all neobrutalism-shadow-sm cursor-pointer",
disabled && "opacity-50 cursor-not-allowed"
)}
style={{ left: `${percentage}%` }}
/>
</div>
)
}
)
Slider.displayName = "Slider"
export { Slider }JavaScript: Copy this code into components/ui/slider.jsx:
jsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef(
({ className, value, defaultValue, onValueChange, min = 0, max = 100, step = 1, disabled, ...props }, ref) => {
const [internalValue, setInternalValue] = React.useState(
value?.[0] ?? defaultValue?.[0] ?? Number(min)
)
React.useEffect(() => {
if (value !== undefined && value[0] !== undefined) {
setInternalValue(value[0])
}
}, [value])
const handleChange = (e) => {
if (disabled) return
const newValue = Number(e.target.value)
setInternalValue(newValue)
onValueChange?.([newValue])
}
const percentage = ((internalValue - Number(min)) / (Number(max) - Number(min))) * 100
return (
<div className={cn("relative flex w-full items-center", className)}>
<input
type="range"
min={min}
max={max}
step={step}
value={internalValue}
onChange={handleChange}
disabled={disabled}
ref={ref}
className="sr-only"
{...props}
/>
<div className="relative h-2 w-full rounded-full border-2 border-foreground bg-muted neobrutalism-shadow-sm">
<div
className={cn(
"absolute h-full rounded-full border-2 border-foreground bg-primary transition-all neobrutalism-shadow-sm",
disabled && "opacity-50"
)}
style={{ width: `${percentage}%` }}
/>
</div>
<div
className={cn(
"absolute h-5 w-5 -translate-x-1/2 transform rounded-sm border-2 border-foreground bg-background transition-all neobrutalism-shadow-sm cursor-pointer",
disabled && "opacity-50 cursor-not-allowed"
)}
style={{ left: `${percentage}%` }}
/>
</div>
)
}
)
Slider.displayName = "Slider"
export { Slider }Usage
TypeScript:
tsx
import { Slider } from "@/components/ui/slider"
import { useState } from "react"
function MyComponent() {
const [value, setValue] = useState([50])
return (
<Slider
value={value}
onValueChange={setValue}
min={0}
max={100}
step={1}
/>
)
}JavaScript:
jsx
import { Slider } from "@/components/ui/slider"
import { useState } from "react"
function MyComponent() {
const [value, setValue] = useState([50])
return (
<Slider
value={value}
onValueChange={setValue}
min={0}
max={100}
step={1}
/>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.
Examples
50%
25%
50%
100