Secret Landing Intro
A paper-textured intro overlay with a handwritten prompt and hand-drawn curly arrow pointing to a black dot — click it to play a circular clip-path reveal that expands into the real landing page.
Page TransitionReactTailwind CSS
CSSTailwind
Manual
Create a file and paste the following code into it.
components/secret-landing-intro.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"use client";
import {
useEffect,
useRef,
useState,
type CSSProperties,
type RefObject,
} from "react";
import { cn } from "@/lib/cn";
const REVEAL_DURATION_MS = 1400;
const REVEAL_EASE = "cubic-bezier(0.7, 0, 0.15, 1)";
const HAND_FONT =
"'Bradley Hand', 'Kalam', 'Caveat', 'Comic Sans MS', 'Chalkboard SE', cursive, system-ui, sans-serif";
const INTRO_BG_STYLE: CSSProperties = {
backgroundColor: "#d2bf91",
backgroundImage:
"radial-gradient(ellipse at 22% 18%, rgba(255, 248, 216, 0.55) 0%, rgba(255, 248, 216, 0) 52%), radial-gradient(ellipse at 80% 85%, rgba(86, 52, 16, 0.38) 0%, rgba(86, 52, 16, 0) 58%)",
};
const GRAIN_STYLE: CSSProperties = {
backgroundImage:
"radial-gradient(ellipse at 50% 120%, rgba(50, 30, 4, 0.55) 0%, rgba(50, 30, 4, 0) 60%)",
};
const LANDING_BG_STYLE: CSSProperties = {
backgroundImage:
"linear-gradient(180deg, #2a1c10 0%, #3d2716 14%, #5c3820 30%, #8a5028 48%, #c0713a 64%, #e08a48 74%, #4a2a12 100%)",
};
function CurlyArrow({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 608 605"
fill="currentColor"
className={cn("block", className)}
aria-hidden
>
<path d="M242.15 604.624c-50.565.301-96.713-35.215-112.395-82.784C40.038 570.236-13.849 488.167 3.107 403.73c2.05-28.341 47.639-172.64 83.134-160.771 7.224 4.047 13.02 28.685 1.357 30.39-7.101 1.277-10.055-5.796-10.026-11.714-36.493 49.877-64.51 127.527-60.163 189.692 5.029 60.529 56.02 86.908 107.822 54.519-8.177-37.637-1.828-78.632 18.938-111.256 13.598-20.417 37.767-46.292 64.718-37.667 29.133 13.2 12.261 51.37 3.417 73.212-14.881 33.043-38.541 62.282-68.362 82.961 16.81 59.947 84.921 91.436 141.917 67.383 64.236-26.617 107.262-88.066 134.599-149.54-35.817-31.447-41.054-84.166-34.153-128.755 4.087-21.159 9.345-45.89 28.96-58.087 19.119-10.847 37.593 8.411 41.625 26.426 13.991 51.034 1.084 105.964-16.561 154.565 47.31 26.442 95.991-17.594 119.646-56.45 41.538-65.009 38.647-148.236 9.867-217.684-10.255-27.109-24.057-52.636-37.612-78.19-8.324-15.37-16.631-30.721-24.469-46.343-3.305-5.456-7.078-13.759.584-17.428 4.065-1.895 8.793-.024 10.742 3.923 14.249 28.7 29.971 56.62 44.699 85.075 40.704 77.144 62.327 172.15 23.286 254.253-24.624 55.823-87.792 121.478-152.955 87.928-35.241 77.077-99.531 162.81-191.967 164.452M139.804 496.206c32.156-24.06 73.898-79.22 67.608-121.009l-.156-.161q-.21-.3-.438-.589c.207.286.41.56.608.844-14.417-12.63-33.308 7.872-42.799 18.527-22.644 28.331-30.743 66.915-24.823 102.388m287.076-81.353c11.328-30.408 18.256-62.105 19.57-94.483 3.625-31.166-15.306-101.241-39.086-36.924-12.252 43.028-12.597 96.792 19.516 131.407" />
<path d="M485.154 59.237c-5.267-8.049 2.685-17.492 4.58-25.606 5.193-13.853 8.333-38.395 28.779-32.822 14.04 3.919 26.93 11.207 40.705 16 9.789 3.888 4.156 18.774-5.761 15.108-12.323-4.477-24.123-10.25-36.387-14.83-1.299-.407-2.77-.723-4.134-.698-4.864 12.4-8.616 25.235-13.415 37.678.198 7.259-9.81 11.105-14.367 5.17" />
</svg>
);
}
function PersistentSecretText() {
return (
<p
className="pointer-events-none absolute left-1/2 top-[17%] z-30 w-full max-w-[640px] -translate-x-1/2 px-6 text-center text-[clamp(1.75rem,4.8vw,2.6rem)] italic leading-tight text-[#fdfaee]"
style={{
fontFamily: HAND_FONT,
textShadow:
"0 2px 14px rgba(20, 10, 0, 0.65), 0 0 32px rgba(20, 10, 0, 0.35)",
}}
>
can you keep a secret?
</p>
);
}
type IntroOverlayProps = {
dotRef: RefObject<HTMLButtonElement | null>;
onReveal: () => void;
};
function IntroOverlay({ dotRef, onReveal }: IntroOverlayProps) {
return (
<div className="absolute inset-0 overflow-hidden" style={INTRO_BG_STYLE}>
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.18] mix-blend-multiply"
style={GRAIN_STYLE}
/>
<div className="absolute left-1/2 top-[33%] flex -translate-x-1/2 flex-col items-center gap-8">
<div
aria-hidden
className="pointer-events-none relative size-[170px] text-[#fdfaee]"
style={{ filter: "drop-shadow(0 4px 10px rgba(30, 18, 0, 0.28))" }}
>
<div
className="absolute inset-0"
style={{ transform: "translateX(-10px) rotate(140deg)" }}
>
<CurlyArrow className="size-full" />
</div>
</div>
<button
ref={dotRef}
type="button"
onClick={onReveal}
aria-label="Reveal the landing page"
className={cn(
"relative size-[24px] rounded-full",
"bg-[radial-gradient(circle_at_50%_35%,#120c06_0%,#050301_100%)]",
"shadow-[inset_0_2px_3px_rgba(0,0,0,0.9),inset_0_-1px_1px_rgba(255,235,180,0.22),0_1px_0_rgba(255,248,220,0.55),0_-1px_0.5px_rgba(60,35,8,0.4),0_2px_3px_rgba(255,245,210,0.15)]",
"transition-[transform,filter] duration-200 ease-out hover:brightness-125 active:scale-[0.97] active:brightness-90",
)}
/>
</div>
</div>
);
}
function LandingContent({ onBack }: { onBack: () => void }) {
return (
<div
className="relative h-full w-full overflow-hidden"
style={LANDING_BG_STYLE}
>
<div
aria-hidden
className="pointer-events-none absolute left-1/2 top-[58%] h-[440px] w-[440px] -translate-x-1/2 -translate-y-1/2 rounded-full blur-2xl"
style={{
backgroundImage:
"radial-gradient(circle, rgba(255, 210, 130, 0.55) 0%, rgba(255, 170, 85, 0.25) 35%, rgba(255, 140, 55, 0) 66%)",
}}
/>
<svg
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-[22%] h-[26%] w-full"
viewBox="0 0 1200 220"
preserveAspectRatio="none"
>
<path
d="M0,220 L0,130 L90,85 L180,115 L270,60 L360,100 L460,55 L560,105 L660,55 L780,110 L900,60 L1020,105 L1120,75 L1200,100 L1200,220 Z"
fill="rgba(35, 20, 8, 0.7)"
/>
</svg>
<svg
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-[28%] w-full"
viewBox="0 0 1200 220"
preserveAspectRatio="none"
>
<path
d="M0,220 L0,145 L120,85 L210,125 L330,55 L450,105 L570,50 L690,110 L810,55 L930,115 L1050,65 L1170,100 L1200,80 L1200,220 Z"
fill="#0a0402"
/>
</svg>
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.15] mix-blend-multiply"
style={{
backgroundImage:
"radial-gradient(ellipse at 50% 120%, rgba(30, 15, 0, 0.55) 0%, rgba(30, 15, 0, 0) 60%)",
}}
/>
<button
type="button"
onClick={onBack}
className="absolute left-6 top-6 z-20 flex items-center gap-2 rounded-full border border-white/15 bg-black/30 px-4 py-2 text-xs font-medium text-white/80 backdrop-blur-md transition-colors hover:border-white/30 hover:bg-black/45 hover:text-white"
>
<span aria-hidden>←</span>
<span>back to the secret</span>
</button>
<div className="absolute left-1/2 top-[42%] z-10 flex w-full -translate-x-1/2 flex-col items-center gap-4 px-6 text-center">
<span className="text-[11px] font-semibold uppercase tracking-[0.42em] text-[#fdfaee]/55">
chapter 01
</span>
<p
className="max-w-md text-base italic text-[#fdfaee]/85 md:text-lg"
style={{ fontFamily: HAND_FONT }}
>
a quiet place, kept between us.
</p>
<button
type="button"
className="mt-1 rounded-full bg-[#fdfaee] px-5 py-2.5 text-sm font-semibold text-[#3a2010] transition-[transform,background-color] duration-150 ease-out hover:bg-[#fff5d8] active:scale-[0.97] active:bg-[#e8dfc4]"
>
begin the story
</button>
</div>
</div>
);
}
export default function SecretLandingIntro() {
const containerRef = useRef<HTMLDivElement>(null);
const dotRef = useRef<HTMLButtonElement>(null);
const [isRevealed, setIsRevealed] = useState(false);
const [origin, setOrigin] = useState({ x: 50, y: 50 });
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const measure = () => {
const dot = dotRef.current;
if (!container || !dot) return;
const cRect = container.getBoundingClientRect();
if (cRect.width === 0 || cRect.height === 0) return;
const dRect = dot.getBoundingClientRect();
const cx = dRect.left + dRect.width / 2 - cRect.left;
const cy = dRect.top + dRect.height / 2 - cRect.top;
const nextX = (cx / cRect.width) * 100;
const nextY = (cy / cRect.height) * 100;
setOrigin((prev) =>
prev.x === nextX && prev.y === nextY ? prev : { x: nextX, y: nextY },
);
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(container);
return () => ro.disconnect();
}, []);
const revealRadius = isRevealed ? 160 : 0;
const revealStyle: CSSProperties = {
clipPath: `circle(${revealRadius}% at ${origin.x}% ${origin.y}%)`,
WebkitClipPath: `circle(${revealRadius}% at ${origin.x}% ${origin.y}%)`,
transition: `clip-path ${REVEAL_DURATION_MS}ms ${REVEAL_EASE}, -webkit-clip-path ${REVEAL_DURATION_MS}ms ${REVEAL_EASE}`,
pointerEvents: isRevealed ? "auto" : "none",
};
return (
<div ref={containerRef} className="relative h-dvh w-full overflow-hidden">
<IntroOverlay dotRef={dotRef} onReveal={() => setIsRevealed(true)} />
<div className="absolute inset-0" style={revealStyle}>
<LandingContent onBack={() => setIsRevealed(false)} />
</div>
<PersistentSecretText />
</div>
);
}Update the import paths to match your project setup.