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';








