Web InnoventixFreeCode

Brightness Slider

Original · free

brightness/contrast control sliders

byWeb InnoventixReact + Tailwind
sldbrightnesssliders
Open

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.json
sld-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 &amp; 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 quote

Similar components

Browse all →