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