Brightness Slider
Original · freebrightness/contrast control sliders
byWeb InnoventixReact + Tailwind
sldbrightnesssliders
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/sld-brightness.jsonsld-brightness.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type Key = "brightness" | "contrast" | "saturate" | "warmth";
type Values = Record<Key, number>;
const DEFAULTS: Values = { brightness: 100, contrast: 100, saturate: 100, warmth: 0 };
const iconSun: ReactNode = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4" aria-hidden="true">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
);
const iconContrast: ReactNode = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} className="h-4 w-4" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M12 3a9 9 0 0 1 0 18Z" fill="currentColor" stroke="none" />
</svg>
);
const iconDroplet: ReactNode = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4" aria-hidden="true">
<path d="M12 3s6 5.4 6 10a6 6 0 0 1-12 0c0-4.6 6-10 6-10Z" />
</svg>
);
const iconTemp: ReactNode = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4" aria-hidden="true">
<path d="M10 13.6V5a2 2 0 1 1 4 0v8.6a4 4 0 1 1-4 0Z" />
<path d="M12 9v5.4" />
</svg>
);
type Slider = { key: Key; label: string; min: number; max: number; unit: string; icon: ReactNode };
const SLIDERS: Slider[] = [
{ key: "brightness", label: "Brightness", min: 0, max: 200, unit: "%", icon: iconSun },
{ key: "contrast", label: "Contrast", min: 0, max: 200, unit: "%", icon: iconContrast },
{ key: "saturate", label: "Saturation", min: 0, max: 200, unit: "%", icon: iconDroplet },
{ key: "warmth", label: "Warmth", min: 0, max: 100, unit: "", icon: iconTemp },
];
type Preset = { id: string; label: string; values: Values };
const PRESETS: Preset[] = [
{ id: "original", label: "Original", values: { brightness: 100, contrast: 100, saturate: 100, warmth: 0 } },
{ id: "vivid", label: "Vivid", values: { brightness: 106, contrast: 122, saturate: 152, warmth: 0 } },
{ id: "noir", label: "Noir", values: { brightness: 102, contrast: 138, saturate: 0, warmth: 0 } },
{ id: "golden", label: "Golden", values: { brightness: 107, contrast: 104, saturate: 120, warmth: 38 } },
{ id: "fade", label: "Fade", values: { brightness: 105, contrast: 86, saturate: 80, warmth: 10 } },
];
function fmt(key: Key, v: number, unit: string): string {
if (key === "warmth") return `${v}`;
return `${v}${unit}`;
}
export default function SldBrightness() {
const reduce = useReducedMotion();
const [values, setValues] = useState<Values>(DEFAULTS);
const [preset, setPreset] = useState<string>("original");
const [compare, setCompare] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const copyTimer = useRef<number | null>(null);
useEffect(() => () => { if (copyTimer.current) window.clearTimeout(copyTimer.current); }, []);
const filter = `brightness(${values.brightness}%) contrast(${values.contrast}%) saturate(${values.saturate}%) sepia(${values.warmth}%)`;
const shown = compare ? "none" : filter;
const changed = SLIDERS.filter((s) => values[s.key] !== DEFAULTS[s.key]).length;
function setOne(key: Key, val: number) {
setValues((v) => ({ ...v, [key]: val }));
setPreset("custom");
}
function applyPreset(p: Preset) {
setValues(p.values);
setPreset(p.id);
}
function resetAll() {
setValues(DEFAULTS);
setPreset("original");
}
function copyCss() {
const css = `filter: ${filter};`;
if (typeof navigator !== "undefined" && navigator.clipboard) {
navigator.clipboard.writeText(css).then(() => {
setCopied(true);
if (copyTimer.current) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1600);
}).catch(() => {});
}
}
const enter = reduce
? {}
: { initial: { opacity: 0, y: 18 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] as const } };
const focusRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
return (
<section className="relative w-full overflow-hidden bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100">
<style>{`
.sldbr-scope .sldbr-range {
-webkit-appearance: none; appearance: none;
width: 100%; height: 6px; border-radius: 9999px; cursor: pointer;
background: linear-gradient(to right, currentColor 0 var(--sldbr-fill), rgba(120,120,140,0.24) var(--sldbr-fill) 100%);
}
.sldbr-scope .sldbr-range::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
height: 18px; width: 18px; border-radius: 9999px; background: currentColor;
border: 3px solid #ffffff; box-shadow: 0 1px 4px rgba(15,23,42,0.35);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.sldbr-scope .sldbr-range::-moz-range-thumb {
height: 18px; width: 18px; border-radius: 9999px; background: currentColor;
border: 3px solid #ffffff; box-shadow: 0 1px 4px rgba(15,23,42,0.35);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.sldbr-scope .sldbr-range:hover::-webkit-slider-thumb { transform: scale(1.12); }
.sldbr-scope .sldbr-range:hover::-moz-range-thumb { transform: scale(1.12); }
.sldbr-scope .sldbr-range:focus-visible { outline: none; }
.sldbr-scope .sldbr-range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 5px rgba(99,102,241,0.35); }
.sldbr-scope .sldbr-range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 5px rgba(99,102,241,0.35); }
.sldbr-scope .sldbr-img { transition: filter 240ms cubic-bezier(0.22,1,0.36,1); }
@keyframes sldbr-pop { 0% { transform: scale(0.5); opacity: 0; } 60% { transform: scale(1.12); } 100% { transform: scale(1); opacity: 1; } }
@keyframes sldbr-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.sldbr-scope .sldbr-pop { animation: sldbr-pop 320ms cubic-bezier(0.22,1,0.36,1) both; }
.sldbr-scope .sldbr-dot { animation: sldbr-pulse 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.sldbr-scope *, .sldbr-scope *::before, .sldbr-scope *::after { animation: none !important; transition: none !important; }
.sldbr-scope .sldbr-range::-webkit-slider-thumb { transition: none !important; }
.sldbr-scope .sldbr-range::-moz-range-thumb { transition: none !important; }
}
`}</style>
<div className="pointer-events-none absolute inset-0 opacity-70 [mask-image:radial-gradient(ellipse_at_top,black,transparent_72%)]">
<div className="absolute -left-24 top-0 h-80 w-80 rounded-full bg-indigo-300/30 blur-3xl dark:bg-indigo-600/20" />
<div className="absolute -right-16 top-24 h-72 w-72 rounded-full bg-violet-300/30 blur-3xl dark:bg-violet-700/20" />
</div>
<div className="sldbr-scope relative mx-auto max-w-6xl px-6 py-20 sm:py-24">
<motion.div {...enter}>
<div className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-slate-600 backdrop-blur dark:border-slate-800 dark:bg-slate-900/70 dark:text-slate-300">
<span className="sldbr-dot h-1.5 w-1.5 rounded-full bg-emerald-500" />
Photo Studio · Live filters
</span>
<h2 className="mt-5 text-3xl font-semibold tracking-tight sm:text-4xl">
Dial in brightness & contrast
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-500 dark:text-slate-400">
Drag any control to grade the preview in real time, stack a preset, then copy the exact CSS filter string straight into your stylesheet.
</p>
</div>
<div className="mt-12 grid gap-6 lg:grid-cols-[1.12fr_1fr]">
{/* Preview */}
<div className="flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center justify-between border-b border-slate-100 px-4 py-3 dark:border-slate-800">
<span className="inline-flex items-center gap-2 text-xs font-medium text-slate-500 dark:text-slate-400">
<span className={`h-2 w-2 rounded-full ${compare ? "bg-slate-400" : "sldbr-dot bg-indigo-500"}`} />
{compare ? "Original" : changed === 0 ? "Unedited" : `${changed} adjustment${changed > 1 ? "s" : ""}`}
</span>
<button
type="button"
onClick={() => setCompare((c) => !c)}
aria-pressed={compare}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${focusRing} ${
compare
? "border-indigo-500 bg-indigo-500 text-white"
: "border-slate-200 bg-slate-50 text-slate-600 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className="h-3.5 w-3.5" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M12 4v16" />
</svg>
Compare
</button>
</div>
<div className="relative aspect-[4/3] w-full overflow-hidden bg-slate-100 dark:bg-slate-800">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/img/gallery/g11.webp"
alt="Mountain lake at golden hour, used as the live editing preview"
loading="lazy"
draggable={false}
className="sldbr-img h-full w-full object-cover"
style={{ filter: shown }}
/>
<div className="pointer-events-none absolute inset-0 ring-1 ring-inset ring-black/5 dark:ring-white/5" />
</div>
<div className="flex items-center gap-3 border-t border-slate-100 px-4 py-3 dark:border-slate-800">
<code className="min-w-0 flex-1 truncate rounded-lg bg-slate-100 px-3 py-2 font-mono text-[11px] text-slate-600 dark:bg-slate-800 dark:text-slate-300">
filter: {filter};
</code>
<button
type="button"
onClick={copyCss}
aria-label="Copy CSS filter to clipboard"
className={`inline-flex shrink-0 items-center gap-1.5 rounded-lg px-3 py-2 text-xs font-semibold transition-colors ${focusRing} ${
copied
? "bg-emerald-500 text-white"
: "bg-slate-900 text-white hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200"
}`}
>
{copied ? (
<>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round" className="sldbr-pop h-3.5 w-3.5" aria-hidden="true">
<path d="M4 12l5 5L20 6" />
</svg>
Copied
</>
) : (
<>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className="h-3.5 w-3.5" aria-hidden="true">
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15V5a2 2 0 0 1 2-2h8" />
</svg>
Copy CSS
</>
)}
</button>
</div>
</div>
{/* Controls */}
<div className="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900 sm:p-6">
<div>
<span className="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
Presets
</span>
<div role="group" aria-label="Filter presets" className="mt-3 flex flex-wrap gap-2">
{PRESETS.map((p) => {
const active = preset === p.id;
return (
<button
key={p.id}
type="button"
onClick={() => applyPreset(p)}
aria-pressed={active}
className={`rounded-full px-3.5 py-1.5 text-sm font-medium transition-colors ${focusRing} ${
active
? "bg-indigo-500 text-white shadow-sm shadow-indigo-500/30"
: "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
}`}
>
{p.label}
</button>
);
})}
</div>
</div>
<div className="my-6 h-px bg-slate-100 dark:bg-slate-800" />
<div className="flex flex-col gap-6">
{SLIDERS.map((s) => {
const v = values[s.key];
const pct = ((v - s.min) / (s.max - s.min)) * 100;
const id = `sldbr-${s.key}`;
const isDefault = v === DEFAULTS[s.key];
return (
<div key={s.key}>
<div className="flex items-center justify-between">
<label htmlFor={id} className="inline-flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-200">
<span className="text-indigo-500 dark:text-indigo-400">{s.icon}</span>
{s.label}
</label>
<button
type="button"
onClick={() => setOne(s.key, DEFAULTS[s.key])}
aria-label={`Reset ${s.label} to default`}
disabled={isDefault}
className={`rounded-md px-2 py-0.5 font-mono text-xs tabular-nums transition-colors ${focusRing} ${
isDefault
? "text-slate-400 dark:text-slate-500"
: "text-indigo-600 hover:bg-indigo-50 dark:text-indigo-300 dark:hover:bg-indigo-500/10"
}`}
>
{fmt(s.key, v, s.unit)}
</button>
</div>
<input
id={id}
type="range"
min={s.min}
max={s.max}
value={v}
onChange={(e) => setOne(s.key, Number(e.target.value))}
aria-valuetext={fmt(s.key, v, s.unit)}
className="sldbr-range mt-3 block w-full text-indigo-500 dark:text-indigo-400"
style={{ "--sldbr-fill": `${pct}%` } as CSSProperties}
/>
</div>
);
})}
</div>
<div className="mt-7 flex items-center justify-between">
<p className="text-xs text-slate-400 dark:text-slate-500">
Use arrow keys to fine-tune a focused slider.
</p>
<button
type="button"
onClick={resetAll}
disabled={changed === 0}
className={`inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-semibold transition-colors ${focusRing} ${
changed === 0
? "cursor-not-allowed border-slate-200 text-slate-400 dark:border-slate-800 dark:text-slate-600"
: "border-slate-200 text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
}`}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" className="h-3.5 w-3.5" aria-hidden="true">
<path d="M3 12a9 9 0 1 0 3-6.7" />
<path d="M3 4v4h4" />
</svg>
Reset all
</button>
</div>
</div>
</div>
</motion.div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Slider
Originalbasic range slider with value bubble

Range Dual Slider
Originaldual-handle min/max range

Steps Slider
Originalstepped slider with tick marks

Vertical Slider
Originalvertical sliders

Tooltip Slider
Originalslider with a floating value tooltip

Volume Slider
Originalvolume slider with icon

Price Range Slider
Originalprice range filter slider

Gradient Track Slider
Originalslider with a gradient track

Rating Slider
Originalsatisfaction rating slider

Marks Slider
Originalslider with labelled marks

Color Hue Slider
Originalhue/saturation color sliders

Progress Drag Slider
Originalmedia scrubber slider

