Components
A mouse-tracked before/after comparison component with a diagonal clip-path divider. Hover to reveal — the line follows your cursor and snaps back with spring easing on mouse leave. Pure React + CSS, no dependencies.
Loading preview...
import { useState, useRef, useCallback } from "react";
/* ═══════════════════════════════════════════════════════════════════════════ */
/* DiagonalReveal */
/* ═══════════════════════════════════════════════════════════════════════════ */
function DiagonalReveal({
after,
before,
caption,
angle = 18,
rest = 50,
lineColor = "#8b5cf6",
lineWidth = 2.5,
height = 480,
borderRadius = 20,
showHint = true,
hintText = "← drag →",
snapDuration = 500,
}) {
const [pos, setPos] = useState(rest);
const [hovering, setHovering] = useState(false);
const ref = useRef(null);
const skew = Math.tan((angle * Math.PI) / 180) * 50;
const snap = `${snapDuration}ms cubic-bezier(0.16,1,0.3,1)`;
const calc = useCallback(
(cx) => {
if (!ref.current) return pos;
const r = ref.current.getBoundingClientRect();
return Math.max(1, Math.min(99, ((cx - r.left) / r.width) * 100));
},
[pos]
);
const handleTouch = useCallback(
(e) => {
if (e.touches.length > 0) setPos(calc(e.touches[0].clientX));
},
[calc]
);
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 14 }}>
<div
ref={ref}
style={{
position: "relative", width: "100%", height, borderRadius,
border: "1px solid rgba(0,0,0,0.07)", background: "#f3f3f3",
overflow: "hidden", cursor: "ew-resize", userSelect: "none", touchAction: "none",
}}
onMouseEnter={() => setHovering(true)}
onMouseMove={(e) => setPos(calc(e.clientX))}
onMouseLeave={() => { setHovering(false); setPos(rest); }}
onTouchStart={(e) => { setHovering(true); handleTouch(e); }}
onTouchMove={handleTouch}
onTouchEnd={() => { setHovering(false); setPos(rest); }}
>
<div style={{ position: "absolute", inset: 0, zIndex: 0 }}>{before}</div>
<div style={{
position: "absolute", inset: 0, zIndex: 1,
clipPath: `polygon(0 0, ${pos + skew}% 0, ${pos - skew}% 100%, 0 100%)`,
transition: hovering ? "none" : `clip-path ${snap}`,
}}>{after}</div>
<svg style={{
position: "absolute", inset: 0, width: "100%", height: "100%",
zIndex: 2, pointerEvents: "none",
transition: hovering ? "none" : `all ${snap}`,
}}>
<line
x1={`${pos + skew}%`} y1="0" x2={`${pos - skew}%`} y2="100%"
stroke={lineColor} strokeWidth={lineWidth}
style={{ filter: `drop-shadow(0 0 8px ${lineColor}30)` }}
/>
</svg>
{showHint && (
<div style={{
position: "absolute", bottom: 16, left: "50%", transform: "translateX(-50%)",
zIndex: 3, pointerEvents: "none",
opacity: hovering ? 0 : 1, transition: "opacity 400ms ease",
}}>
<span style={{
display: "inline-block", padding: "5px 16px", borderRadius: 99,
fontSize: 11, fontWeight: 600, letterSpacing: "0.18em",
textTransform: "uppercase", color: "#aaa",
background: "rgba(255,255,255,0.75)", backdropFilter: "blur(6px)",
}}>{hintText}</span>
</div>
)}
</div>
{caption && (
<p style={{ fontSize: 13, color: "#888", textAlign: "center", margin: 0 }}>{caption}</p>
)}
</div>
);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* Generic Before / After Layers */
/* ═══════════════════════════════════════════════════════════════════════════ */
const layerBase = {
width: "100%", height: "100%",
display: "flex", alignItems: "center", justifyContent: "center",
padding: 32, fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
};
function BeforeLayer() {
return (
<div style={{ ...layerBase, background: "#f3f3f3" }}>
<div style={{
width: 380, background: "white", borderRadius: 16,
border: "1px solid rgba(0,0,0,0.06)", padding: "24px 24px 20px",
boxShadow: "0 1px 4px rgba(0,0,0,0.03)",
}}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<div>
<div style={{ fontSize: 10, fontWeight: 600, letterSpacing: "0.12em", textTransform: "uppercase", color: "#ccc" }}>Status</div>
<div style={{ fontSize: 16, fontWeight: 600, color: "#ccc", marginTop: 2 }}>Before</div>
</div>
<span style={{
padding: "3px 10px", borderRadius: 99, fontSize: 10, fontWeight: 600,
background: "rgba(239,68,68,0.08)", color: "#ef4444",
}}>Slow</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6, marginBottom: 14 }}>
{[0.92, 0.78, 0.65, 0.45].map((w, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{ width: 28, height: 28, borderRadius: 6, background: "#f5f5f5", flexShrink: 0 }} />
<div style={{ flex: 1 }}>
<div style={{ height: 8, borderRadius: 4, background: "#f0f0f0", width: `${w * 100}%` }} />
<div style={{ height: 6, borderRadius: 3, background: "#f5f5f5", width: `${w * 60}%`, marginTop: 4 }} />
</div>
<div style={{ fontSize: 10, color: "#ddd" }}>???</div>
</div>
))}
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12 }}>
<div style={{ flex: 1, height: 4, borderRadius: 99, background: "#f0f0f0" }}>
<div style={{ width: "12%", height: "100%", borderRadius: 99, background: "#ddd" }} />
</div>
<span style={{ fontSize: 10, color: "#ccc" }}>12%</span>
</div>
<div style={{ display: "flex", gap: 6 }}>
{["Manual", "Error-prone", "Tedious"].map(t => (
<span key={t} style={{
padding: "3px 10px", borderRadius: 99, fontSize: 10, fontWeight: 600,
background: "rgba(0,0,0,0.03)", color: "#ccc",
}}>{t}</span>
))}
</div>
</div>
</div>
);
}
function AfterLayer({ accent = "#8b5cf6" }) {
return (
<div style={{ ...layerBase, background: "#f3f3f3" }}>
<div style={{
width: 380, background: "white", borderRadius: 16,
border: "1px solid rgba(0,0,0,0.06)", padding: "24px 24px 20px",
boxShadow: "0 1px 4px rgba(0,0,0,0.03)",
}}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<div>
<div style={{ fontSize: 10, fontWeight: 600, letterSpacing: "0.12em", textTransform: "uppercase", color: "#888" }}>Status</div>
<div style={{ fontSize: 16, fontWeight: 600, color: "#222", marginTop: 2 }}>After</div>
</div>
<span style={{
padding: "3px 10px", borderRadius: 99, fontSize: 10, fontWeight: 600,
background: accent + "12", color: accent,
}}>Instant</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6, marginBottom: 14 }}>
{[
["Design System", "Completed"],
["API Integration", "Completed"],
["User Testing", "Completed"],
["Deployment", "Completed"],
].map(([label, status], i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{
width: 28, height: 28, borderRadius: 6,
background: accent + "10", display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 12, color: accent, fontWeight: 700, flexShrink: 0,
}}>{i + 1}</div>
<div style={{ flex: 1, fontSize: 13, color: "#444", fontWeight: 500 }}>{label}</div>
<span style={{ fontSize: 10, color: "#16a34a", fontWeight: 600 }}>✓ {status}</span>
</div>
))}
</div>
<div style={{ display: "flex", gap: 8, alignItems: "center", marginBottom: 12 }}>
<div style={{ flex: 1, height: 4, borderRadius: 99, background: "#f0f0f0" }}>
<div style={{ width: "100%", height: "100%", borderRadius: 99, background: accent }} />
</div>
<span style={{ fontSize: 10, color: accent, fontWeight: 600 }}>100%</span>
</div>
<div style={{ display: "flex", gap: 6 }}>
{["Automated", "Reliable", "Fast"].map(t => (
<span key={t} style={{
padding: "3px 10px", borderRadius: 99, fontSize: 10, fontWeight: 600,
background: "#ecfdf5", color: "#16a34a",
}}>{t}</span>
))}
</div>
</div>
</div>
);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* Playground */
/* ═══════════════════════════════════════════════════════════════════════════ */
const presets = [
{ label: "Default", angle: 18, rest: 50, color: "#8b5cf6", snap: 500 },
{ label: "Subtle", angle: 8, rest: 50, color: "#6366f1", snap: 600 },
{ label: "Dramatic", angle: 24, rest: 50, color: "#ec4899", snap: 400 },
{ label: "Reveal", angle: 18, rest: 75, color: "#0ea5e9", snap: 500 },
{ label: "Warm", angle: 14, rest: 40, color: "#f59e0b", snap: 450 },
];
export default function App() {
const [preset, setPreset] = useState(0);
const p = presets[preset];
return (
<div style={{
minHeight: "100vh", background: "#fafafa",
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
}}>
<div style={{ maxWidth: 720, margin: "0 auto", padding: "48px 24px" }}>
{/* Header */}
<div style={{ textAlign: "center", marginBottom: 36 }}>
<div style={{
display: "inline-block", padding: "4px 14px", borderRadius: 99,
fontSize: 11, fontWeight: 600, letterSpacing: "0.1em",
background: p.color + "10", color: p.color, marginBottom: 12,
transition: "all 300ms ease",
}}>DIAGONAL REVEAL</div>
<h1 style={{
fontSize: 28, fontWeight: 300, letterSpacing: "-0.02em",
color: "#222", margin: "0 0 6px",
}}>Before / After Comparison</h1>
<p style={{ fontSize: 14, color: "#999", margin: 0, lineHeight: 1.6 }}>
Hover to reveal. Mouse-tracked diagonal clip with spring snap-back.
</p>
</div>
{/* Preset tabs */}
<div style={{
display: "flex", flexWrap: "wrap", justifyContent: "center",
gap: 6, marginBottom: 24,
}}>
{presets.map((pr, i) => (
<button
key={pr.label}
onClick={() => setPreset(i)}
style={{
padding: "7px 18px", borderRadius: 99, border: "none",
fontSize: 12, fontWeight: 500, cursor: "pointer",
background: preset === i ? pr.color + "15" : "rgba(0,0,0,0.03)",
color: preset === i ? pr.color : "#999",
transition: "all 200ms ease",
}}
>{pr.label}</button>
))}
</div>
{/* Component */}
<DiagonalReveal
before={<BeforeLayer />}
after={<AfterLayer accent={p.color} />}
angle={p.angle}
rest={p.rest}
lineColor={p.color}
height={480}
snapDuration={p.snap}
caption={`angle: ${p.angle}° · rest: ${p.rest}% · snap: ${p.snap}ms`}
/>
{/* Prop display */}
<div style={{
marginTop: 36, padding: "20px 24px", borderRadius: 12,
background: "white", border: "1px solid rgba(0,0,0,0.06)",
}}>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: "0.1em", textTransform: "uppercase", color: "#bbb", marginBottom: 12 }}>Current Props</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "10px 16px" }}>
{[
["angle", `${p.angle}°`],
["rest", `${p.rest}%`],
["lineColor", p.color],
["snapDuration", `${p.snap}ms`],
["height", "480px"],
["borderRadius", "20px"],
].map(([k, v]) => (
<div key={k} style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: 12, color: "#999" }}>{k}</span>
<span style={{
fontSize: 12, fontWeight: 600, fontFamily: "monospace",
color: k === "lineColor" ? p.color : "#444",
}}>{v}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}