Kiki

3D Before & After Image Slider

Component 32

About

A prism-style image comparison. Twenty-four shaded facets rotate between two views as you drag or move the slider, with clean A/B endpoints. Includes keyboard controls, responsive sizing, reduced-motion support and image-loading recovery. Use your own image pair for before-and-after edits, material studies or visual comparisons. The demo includes three original landscape studies and color grades.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds 3D Before & After Image Slider to your project and fits it to your theme.

Code

How 3D Before & After Image Slider is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import { useState } from "react";
import { LenticularCompare } from "@/components/ui/lenticular-compare";
const studies = [
  {
    name: "Coast",
    src: "https://cdn.21st.dev/assets/mirror/cf/cf326af6b2be3c5915024de34d605f209e030bec387b2a18b936095d906dba65.webp",
    alt: "Limestone architecture beside a cobalt-blue sea",
    filter: "sepia(.42) saturate(.63) contrast(1.12) brightness(.83)",
  },
  {
    name: "Dunes",
    src: "https://cdn.21st.dev/assets/mirror/18/1857ca64dd082de84283b1a9e804aee83c56ec7dd480dfdc0384f573aacbf270.webp",
    alt: "Burnt-orange sand dunes crossed by a dark road",
    filter: "hue-rotate(-18deg) saturate(.55) contrast(1.22) brightness(.78)",
  },
  {
    name: "Water",
    src: "https://cdn.21st.dev/assets/mirror/12/124a2f0a8f44ac6c1fc5fe60068eebabedbe424adced72942895362c021dfe22.webp",
    alt: "A pale geometric platform above deep teal water",
    filter:
      "sepia(.3) hue-rotate(318deg) saturate(.67) contrast(1.19) brightness(.77)",
  },
];
export default function LenticularDemo() {
  const [selected, setSelected] = useState(0),
    study = studies[selected];
  return (
    <div className="lp-demo">
      <style>{DEMO_CSS}</style>
      <main className="demo-shell">
        <header className="demo-header">
          <h1>
            Lenticular<span> /</span>
          </h1>
          <span className="study-number">
            0{selected + 1}
            <i />
            03
          </span>
        </header>
        <LenticularCompare
          key={study.name}
          before={{ src: study.src, alt: study.alt, label: "Original" }}
          after={{
            src: study.src,
            alt: study.alt + " with a muted, darker color grade",
            label: "Grade",
            filter: study.filter,
          }}
        />
        <footer className="preset-footer">
          <div className="preset-list" role="group" aria-label="Image studies">
            {studies.map((item, i) => (
              <button
                key={item.name}
                className={selected === i ? "active" : ""}
                aria-pressed={selected === i}
                onClick={() => setSelected(i)}
              >
                <span className="preset-image">
                  <img src={item.src} alt="" />
                </span>
                <span className="preset-name">
                  <small>0{i + 1}</small>
                  {item.name}
                </span>
                <span className="preset-marker" />
              </button>
            ))}
          </div>
          <span className="footer-count">
            {String(selected + 1).padStart(2, "0")} /{" "}
            {String(studies.length).padStart(2, "0")}
          </span>
        </footer>
      </main>
    </div>
  );
}

const DEMO_CSS =
  '.lp-demo{box-sizing:border-box;width:100%;min-height:100%;background:#111211;color:#edeee7;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;}\n.lp-demo * {\n  box-sizing: border-box;\n  scrollbar-width: none;\n}\n.lp-demo *::-webkit-scrollbar {\n  display: none;\n}\n.lp-demo button,.lp-demo input {\n  font: inherit;\n}\n.lp-demo button {\n  background: none;\n  border: 0;\n  color: inherit;\n  cursor: pointer;\n  padding: 0;\n  -webkit-tap-highlight-color: transparent;\n}\n.lp-demo button:focus-visible,.lp-demo input:focus-visible {\n  outline: 2px solid #d1efaa;\n  outline-offset: 5px;\n}\n.lp-demo button:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n.lp-demo h1,.lp-demo p {\n  margin: 0;\n}\n.lp-demo .demo-shell {\n  width: min(100% - 100px, 1180px);\n  margin: auto;\n  padding: 42px 0 35px;\n}\n.lp-demo .demo-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 45px;\n}\n.lp-demo .demo-header h1 {\n  font:\n    500 43px/1 ui-sans-serif,\n    sans-serif;\n  letter-spacing: -2px;\n}\n.lp-demo .demo-header h1 > span {\n  font-weight: 300;\n  color: #666e5d;\n  margin-left: 4px;\n}\n.lp-demo .study-number {\n  display: flex;\n  align-items: center;\n  gap: 15px;\n  font:\n    11px ui-monospace,\n    monospace;\n  color: #a9b19e;\n}\n.lp-demo .study-number i {\n  width: 26px;\n  height: 1px;\n  background: #535e46;\n  transform: rotate(-28deg);\n}\n.lp-demo .preset-footer {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding-top: 29px;\n}\n.lp-demo .preset-list {\n  display: flex;\n  gap: 34px;\n}\n.lp-demo .preset-list > button {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  position: relative;\n  min-height: 52px;\n  padding: 3px 0;\n  color: #8c9c7b;\n  text-align: left;\n  transition: color 0.25s;\n}\n.lp-demo .preset-list > button.active {\n  color: #e1eccf;\n}\n.lp-demo .preset-image {\n  display: block;\n  width: 66px;\n  height: 44px;\n  overflow: hidden;\n  background: #343e29;\n  position: relative;\n}\n.lp-demo .preset-image:after {\n  content: "";\n  position: absolute;\n  inset: 0;\n  border: 1px solid #eff4e317;\n}\n.lp-demo .preset-image img {\n  width: 100%;\n  height: 100%;\n  display: block;\n  object-fit: cover;\n  opacity: 0.55;\n  filter: saturate(0.7);\n  transition:\n    opacity 0.3s,\n    filter 0.3s,\n    transform 0.5s;\n}\n.lp-demo .preset-list > button.active .preset-image img,.lp-demo .preset-list > button:hover .preset-image img {\n  opacity: 1;\n  filter: saturate(1);\n}\n.lp-demo .preset-list > button:hover .preset-image img {\n  transform: scale(1.08);\n}\n.lp-demo .preset-name {\n  display: grid;\n  gap: 5px;\n  font-size: 12px;\n}\n.lp-demo .preset-name small {\n  font:\n    8px ui-monospace,\n    monospace;\n  letter-spacing: 1px;\n  color: #7e9368;\n}\n.lp-demo .preset-marker {\n  position: absolute;\n  bottom: -8px;\n  left: 0;\n  right: 0;\n  height: 1px;\n  background: #a8c385;\n  transform: scaleX(0);\n  transform-origin: left;\n  transition: transform 0.3s;\n}\n.lp-demo .preset-list > button.active .preset-marker {\n  transform: scaleX(1);\n}\n.lp-demo .footer-count {\n  font:\n    9px ui-monospace,\n    monospace;\n  color: #82966d;\n  letter-spacing: 1px;\n}\n@media (min-width: 1600px) {\n  .lp-demo .demo-shell {\n    width: min(100% - 160px, 1340px);\n    padding-top: 50px;\n  }\n  .lp-demo .demo-header {\n    margin-bottom: 55px;\n  }\n  .lp-demo .demo-header h1 {\n    font-size: 52px;\n  }\n  .lp-demo .preset-image {\n    width: 80px;\n    height: 54px;\n  }\n  .lp-demo .preset-name {\n    font-size: 14px;\n  }\n  .lp-demo .preset-name small {\n    font-size: 10px;\n  }\n  .lp-demo .preset-list {\n    gap: 44px;\n  }\n}\n@media (max-width: 1000px) {\n  .lp-demo .demo-shell {\n    width: calc(100% - 64px);\n    padding-top: 32px;\n  }\n  .lp-demo .demo-header {\n    margin-bottom: 31px;\n  }\n  .lp-demo .demo-header h1 {\n    font-size: 38px;\n  }\n  .lp-demo .preset-list {\n    gap: 25px;\n  }\n  .lp-demo .preset-image {\n    width: 60px;\n    height: 40px;\n  }\n  .lp-demo .preset-name {\n    font-size: 11px;\n  }\n  .lp-demo .preset-footer {\n    padding-top: 23px;\n  }\n}\n@media (max-width: 700px) {\n  .lp-demo .demo-shell {\n    width: calc(100% - 36px);\n    padding-top: 28px;\n    padding-bottom: 30px;\n  }\n  .lp-demo .demo-header {\n    margin-bottom: 37px;\n  }\n  .lp-demo .demo-header h1 {\n    font-size: 33px;\n    letter-spacing: -1.7px;\n  }\n  .lp-demo .study-number {\n    font-size: 10px;\n    gap: 12px;\n  }\n  .lp-demo .study-number i {\n    width: 22px;\n  }\n  .lp-demo .preset-footer {\n    padding-top: 24px;\n  }\n  .lp-demo .preset-list {\n    gap: 19px;\n    flex: 1;\n    justify-content: space-between;\n  }\n  .lp-demo .preset-list > button {\n    display: block;\n    min-width: 75px;\n  }\n  .lp-demo .preset-image {\n    width: 100%;\n    height: 53px;\n  }\n  .lp-demo .preset-name {\n    display: flex;\n    gap: 7px;\n    align-items: baseline;\n    margin-top: 11px;\n    font-size: 12px;\n  }\n  .lp-demo .preset-name small {\n    font-size: 9px;\n  }\n  .lp-demo .footer-count {\n    display: none;\n  }\n  .lp-demo .preset-marker {\n    bottom: -11px;\n  }\n  .lp-demo .preset-list > button {\n    max-width: 100px;\n    flex: 1;\n  }\n  .lp-demo .preset-name small {\n    color: #98ad81;\n  }\n}\n@media (max-width: 360px) {\n  .lp-demo .demo-shell {\n    width: calc(100% - 28px);\n  }\n  .lp-demo .demo-header h1 {\n    font-size: 29px;\n  }\n  .lp-demo .demo-header {\n    margin-bottom: 28px;\n  }\n  .lp-demo .preset-list {\n    gap: 13px;\n  }\n  .lp-demo .preset-list > button {\n    min-width: 0;\n  }\n  .lp-demo .preset-name {\n    font-size: 11px;\n    gap: 5px;\n  }\n  .lp-demo .preset-name small {\n    font-size: 8px;\n  }\n  .lp-demo .preset-image {\n    height: 47px;\n  }\n}\n@media (prefers-reduced-motion: reduce) {\n  .lp-demo *,.lp-demo *:before,.lp-demo *:after {\n    animation: none !important;\n    transition: none !important;\n    scroll-behavior: auto !important;\n  }\n  .lp-demo .preset-list > button:hover .preset-image img {\n    transform: none;\n  }\n}\n/* Optical imagery carries the color; the frame remains neutral. */\n.lp-demo .demo-header h1 > span {\n  color: #686b65;\n}\n.lp-demo .study-number {\n  color: #a9ada5;\n}\n.lp-demo .study-number i {\n  background: #555951;\n}\n.lp-demo .preset-list > button {\n  color: #a5ae98;\n}\n.lp-demo .preset-list > button.active {\n  color: #eaf0e0;\n}\n.lp-demo .preset-name small {\n  color: #98a687;\n}\n.lp-demo .preset-marker {\n  background: #d4ddc5;\n}\n.lp-demo .footer-count {\n  color: #a1af91;\n}\n.lp-demo button:focus-visible,.lp-demo input:focus-visible {\n  outline-color: #e2ebd4;\n}\n/* Keep the complete optical interaction in a normal desktop viewport. */\n.lp-demo .demo-shell {\n  width: min(\n    1180px,\n    calc(100% - 80px),\n    max(640px, calc((100svh - 282px) * 1.5))\n  );\n  padding: 24px 0 18px;\n}\n.lp-demo .demo-header {\n  margin-bottom: 20px;\n}\n.lp-demo .demo-header h1 {\n  font-size: 34px;\n}\n.lp-demo .preset-footer {\n  padding-top: 18px;\n}\n.lp-demo .preset-image {\n  width: 62px;\n  height: 42px;\n}\n.lp-demo .preset-list {\n  gap: 30px;\n}\n@media (max-width: 700px) {\n  .lp-demo .demo-shell {\n    width: calc(100% - 36px);\n    padding: 28px 0;\n  }\n  .lp-demo .demo-header {\n    margin-bottom: 27px;\n  }\n  .lp-demo .demo-header h1 {\n    font-size: 33px;\n  }\n  .lp-demo .preset-image {\n    width: 100%;\n    height: 53px;\n  }\n  .lp-demo .preset-footer {\n    padding-top: 22px;\n  }\n  .lp-demo .preset-list {\n    gap: 19px;\n  }\n}\n@media (max-width: 360px) {\n  .lp-demo .demo-shell {\n    width: calc(100% - 28px);\n  }\n  .lp-demo .demo-header h1 {\n    font-size: 29px;\n  }\n  .lp-demo .preset-list {\n    gap: 13px;\n  }\n  .lp-demo .preset-image {\n    height: 47px;\n  }\n}\n';

Dependencies

More by Kiki

View all
  • Filter Token Bar

    Filter Token Bar

    522
  • Compose

    Compose

    423
  • Product Tour

    Product Tour

    216
  • Tier List Maker

    Tier List Maker

    138
  • Draggable Photo Stack

    Draggable Photo Stack

    84
  • Morphing Scroll Navbar

    Morphing Scroll Navbar

    81
  • Selection Bubble Menu

    Selection Bubble Menu

    35
  • Advanced Data Table Filter Builder

    Advanced Data Table Filter Builder

    28