Skip to content

Spend / Save Split Hero

Split screen hero section: two colour fields that spring open on hover or tap, each with its own type, product and CTA.

  • Heroes
  • Fintech
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers (container query units for the giant words). Halves stack and open on tap below 760px.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/hero-split-screen.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/heroes/hero-split-screen.tsx
1/**
2 * HeroSplitScreen: a two-story hero for a bank with two products. Two flat
3 * colour fields, "Spend" and "Save", each with its own type voice, product
4 * fragment and call to action. Hovering, focusing or tapping a half springs
5 * it open and squeezes the other; a round seal rides the seam between them.
6 * On phones the halves stack and the tapped one opens.
7 *
8 * Props: brand, spend and save (label, word, lede, cta, fine print), and the
9 * product data (transactions, pot, balance). Each half is a SplitSide.
10 * CSS vars: --fd-spl-spend, --fd-spl-spend-ink, --fd-spl-save, --fd-spl-save-ink.
11 *
12 * Browser support: all evergreen browsers (container query units for the
13 * giant words; fixed sizes otherwise).
14 * Motion: flex-grow with the library spring curve (overshoot), word size
15 * follows via cqw. The savings balance accrues a cent at a time.
16 * prefers-reduced-motion: halves swap size instantly, no accrual ticking.
17 * Accessibility: each half's title is a button with aria-expanded that opens
18 * it; details stay in the accessibility tree when collapsed.
19 */
20import { useEffect, useId, useState } from "react";
21import type { ReactNode } from "react";
22
23export interface SplitSide {
24 index: string;
25 word: string;
26 lede: ReactNode;
27 cta: { label: string; href: string };
28 fine: ReactNode;
29}
30
31export interface SplitTxn {
32 merchant: string;
33 place: string;
34 amount: string;
35 roundUp: string;
36 time: string;
37 glyph: string;
38}
39
40export interface HeroSplitScreenProps {
41 brand?: string;
42 spend?: SplitSide;
43 save?: SplitSide;
44 transactions?: SplitTxn[];
45 pot?: { name: string; saved: number; goal: number; due: string };
46 balance?: number;
47 rate?: string;
48 defaultActive?: "spend" | "save" | null;
49}
50
51const SPEND: SplitSide = {
52 index: "01",
53 word: "Spend",
54 lede: "A debit card that tells you where the money went before you have left the shop.",
55 cta: { label: "Get the card", href: "#card" },
56 fine: "No fees abroad in 31 countries. Freeze it from your lock screen.",
57};
58
59const SAVE: SplitSide = {
60 index: "02",
61 word: "Save",
62 lede: "Pots that fill themselves with your spare change, at 2.75% paid every month.",
63 cta: { label: "Open a pot", href: "#pots" },
64 fine: "Deposits protected up to €100,000 by the German deposit scheme.",
65};
66
67const TXNS: SplitTxn[] = [
68 { merchant: "Bäckerei Rösler", place: "Prenzlauer Berg", amount: "−€3.40", roundUp: "+€0.60", time: "08:12", glyph: "B" },
69 { merchant: "BVG Monatskarte", place: "Berlin AB", amount: "−€58.00", roundUp: "+€0.00", time: "08:40", glyph: "U" },
70 { merchant: "Dussmann", place: "Friedrichstraße", amount: "−€22.75", roundUp: "+€0.25", time: "13:05", glyph: "D" },
71];
72
73const fmt = (n: number) =>
74 "€" + n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
75
76export function HeroSplitScreen({
77 brand = "Kontor",
78 spend = SPEND,
79 save = SAVE,
80 transactions = TXNS,
81 pot = { name: "Lisbon in June", saved: 1840, goal: 2400, due: "by 14 May" },
82 balance = 4182.36,
83 rate = "2.75% AER",
84 defaultActive = null,
85}: HeroSplitScreenProps) {
86 const [active, setActive] = useState<"spend" | "save" | null>(defaultActive);
87 const [bal, setBal] = useState(balance);
88 const [isTouch, setIsTouch] = useState(false);
89 const ringId = "fd-spl-ring" + useId().replace(/:/g, "");
90
91 useEffect(() => {
92 const mq = window.matchMedia("(max-width: 760px)");
93 const sync = () => {
94 setIsTouch(mq.matches);
95 if (mq.matches) setActive((a) => a ?? "spend");
96 };
97 sync();
98 mq.addEventListener("change", sync);
99 return () => mq.removeEventListener("change", sync);
100 }, []);
101
102 useEffect(() => {
103 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
104 let id = 0;
105 const tick = () => {
106 if (!document.hidden) setBal((b) => Math.round((b + 0.01) * 100) / 100);
107 id = window.setTimeout(tick, 1400);
108 };
109 id = window.setTimeout(tick, 1400);
110 return () => window.clearTimeout(id);
111 }, []);
112
113 const state = active ? `is-${active}` : "is-even";
114 const pct = Math.round((pot.saved / pot.goal) * 100);
115
116 const enter = (s: "spend" | "save") => {
117 if (!isTouch) setActive(s);
118 };
119
120 return (
121 <section
122 className={`fd-spl ${state}`}
123 aria-label={`${brand}: ${spend.word} and ${save.word}`}
124 onPointerLeave={(e) => e.pointerType === "mouse" && !isTouch && setActive(defaultActive)}
125 >
126 <style>{css}</style>
127
128 {/* SPEND */}
129 <div
130 className={`fd-spl-half fd-spl-spend${active === "spend" ? " is-open" : ""}${active === "save" ? " is-shut" : ""}`}
131 onPointerMove={(e) => e.pointerType === "mouse" && active !== "spend" && enter("spend")}
132 onClick={() => isTouch && setActive("spend")}
133 onFocus={() => enter("spend")}
134 >
135 <div className="fd-spl-clip">
136 <header className="fd-spl-top">
137 <a className="fd-spl-logo" href="#top" aria-label={`${brand}, home`}>
138 <svg viewBox="0 0 28 28" width="26" height="26" aria-hidden="true">
139 <rect x="1" y="1" width="26" height="26" rx="6" fill="currentColor" />
140 <path d="M9 7v14M9 14l9-7M12 12l7 9" stroke="#FF5B2E" strokeWidth="2.6" fill="none" strokeLinecap="square" />
141 </svg>
142 <span>{brand.toLowerCase()}</span>
143 </a>
144 <span className="fd-spl-tag">Personal, Business, Kids</span>
145 </header>
146
147 <div className="fd-spl-body">
148 <button
149 type="button"
150 className="fd-spl-head"
151 aria-expanded={active === "spend"}
152 onClick={() => setActive("spend")}
153 >
154 <span className="fd-spl-idx">{spend.index} / {spend.word}</span>
155 <span className="fd-spl-word fd-spl-word-spend">{spend.word}.</span>
156 </button>
157
158 <div className="fd-spl-detail">
159 <div className="fd-spl-detail-in">
160 <p className="fd-spl-lede">{spend.lede}</p>
161 <ul className="fd-spl-feed" aria-label="Today on your card">
162 {transactions.map((t) => (
163 <li key={t.merchant} className="fd-spl-txn">
164 <span className="fd-spl-glyph" aria-hidden="true">{t.glyph}</span>
165 <span className="fd-spl-txn-m">
166 <b>{t.merchant}</b>
167 <span>{t.place}, {t.time}</span>
168 </span>
169 <span className="fd-spl-txn-a">
170 <b>{t.amount}</b>
171 <span>{t.roundUp} to Save</span>
172 </span>
173 </li>
174 ))}
175 </ul>
176 </div>
177 </div>
178
179 <div className="fd-spl-foot">
180 <a className="fd-spl-cta fd-spl-cta-spend" href={spend.cta.href}>
181 {spend.cta.label}
182 <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M3 8h9M8.5 4l4 4-4 4" fill="none" stroke="currentColor" strokeWidth="1.6" /></svg>
183 </a>
184 <p className="fd-spl-fine">{spend.fine}</p>
185 </div>
186 </div>
187 </div>
188 <div className="fd-spl-seal" aria-hidden="true">
189 <svg viewBox="0 0 100 100" width="100" height="100">
190 <defs>
191 <path id={ringId} d="M50,50 m-37,0 a37,37 0 1,1 74,0 a37,37 0 1,1 -74,0" />
192 </defs>
193 <circle cx="50" cy="50" r="49" fill="#1A0F0A" />
194 <text className="fd-spl-seal-t">
195 <textPath href={`#${ringId}`}>ONE ACCOUNT, TWO HALVES, ONE APP, </textPath>
196 </text>
197 <text x="50" y="58" textAnchor="middle" className="fd-spl-seal-or">&amp;</text>
198 </svg>
199 </div>
200 </div>
201
202 {/* SAVE */}
203 <div
204 className={`fd-spl-half fd-spl-save${active === "save" ? " is-open" : ""}${active === "spend" ? " is-shut" : ""}`}
205 onPointerMove={(e) => e.pointerType === "mouse" && active !== "save" && enter("save")}
206 onClick={() => isTouch && setActive("save")}
207 onFocus={() => enter("save")}
208 >
209 <div className="fd-spl-clip">
210 <header className="fd-spl-top fd-spl-top-r">
211 <a className="fd-spl-login" href="#login">Log in</a>
212 <a className="fd-spl-open" href="#open">Open an account</a>
213 </header>
214
215 <div className="fd-spl-body">
216 <button
217 type="button"
218 className="fd-spl-head"
219 aria-expanded={active === "save"}
220 onClick={() => setActive("save")}
221 >
222 <span className="fd-spl-idx">{save.index} / {save.word}</span>
223 <span className="fd-spl-word fd-spl-word-save">{save.word}.</span>
224 </button>
225
226 <div className="fd-spl-detail">
227 <div className="fd-spl-detail-in">
228 <p className="fd-spl-lede">{save.lede}</p>
229 <div className="fd-spl-pot">
230 <div className="fd-spl-pot-row">
231 <span className="fd-spl-pot-name">{pot.name}</span>
232 <span className="fd-spl-pot-due">{pct}%, {pot.due}</span>
233 </div>
234 <div className="fd-spl-bar" role="img" aria-label={`${pct}% of ${fmt(pot.goal)} saved`}>
235 <span style={{ width: `${pct}%` }} />
236 </div>
237 <div className="fd-spl-pot-row fd-spl-pot-nums">
238 <span>{fmt(pot.saved)}</span>
239 <span>of {fmt(pot.goal)}</span>
240 </div>
241 </div>
242 <div className="fd-spl-bal">
243 <span className="fd-spl-bal-l">All pots, earning {rate}</span>
244 <span className="fd-spl-bal-v">{fmt(bal)}</span>
245 </div>
246 </div>
247 </div>
248
249 <div className="fd-spl-foot">
250 <a className="fd-spl-cta fd-spl-cta-save" href={save.cta.href}>
251 {save.cta.label}
252 <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M3 8h9M8.5 4l4 4-4 4" fill="none" stroke="currentColor" strokeWidth="1.6" /></svg>
253 </a>
254 <p className="fd-spl-fine">{save.fine}</p>
255 </div>
256 </div>
257 </div>
258 </div>
259 </section>
260 );
261}
262
263const css = `
264.fd-spl, .fd-spl *, .fd-spl *::before, .fd-spl *::after { box-sizing: border-box; }
265.fd-spl :where(p, a, ul, li, b, span, div, header, button, svg) {
266 margin: 0; padding: 0; border: 0; background: none; font: inherit; color: inherit;
267 letter-spacing: inherit; text-decoration: none; list-style: none; text-align: inherit;
268}
269.fd-spl {
270 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
271 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
272 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
273 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;
274 --fd-spl-spend: #FF5B2E;
275 --fd-spl-spend-ink: #1A0F0A;
276 --fd-spl-save: #0E3B2D;
277 --fd-spl-save-ink: #F1E8D3;
278 --fd-spl-brass: #E1B85C;
279 --fd-spl-sans: "SF Pro Display", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
280 --fd-spl-serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
281 --fd-spl-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
282 position: relative; display: flex; min-height: 880px; margin: 0; padding: 0; overflow: hidden;
283 font: 400 15px/1.45 var(--fd-spl-sans); color: var(--fd-spl-spend-ink); background: var(--fd-spl-save);
284}
285.fd-spl-half {
286 position: relative; flex: 1 1 0; min-width: 0; container-type: inline-size; display: flex; flex-direction: column;
287 transition: flex-grow var(--fd-dur-4) var(--fd-spring);
288}
289.fd-spl.is-spend .fd-spl-spend, .fd-spl.is-save .fd-spl-save { flex-grow: 1.85; }
290.fd-spl-spend { background: var(--fd-spl-spend); color: var(--fd-spl-spend-ink); z-index: 2; }
291.fd-spl-save { background: var(--fd-spl-save); color: var(--fd-spl-save-ink); z-index: 1; }
292.fd-spl-clip { position: relative; flex: 1 1 auto; overflow: hidden; display: flex; flex-direction: column; padding: 0 44px 40px; }
293
294.fd-spl-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 84px; white-space: nowrap; }
295.fd-spl-top-r { justify-content: flex-end; }
296.fd-spl-logo { display: inline-flex; align-items: center; gap: 10px; color: var(--fd-spl-spend-ink); }
297.fd-spl-logo svg { display: block; }
298.fd-spl-logo span { font: 800 24px/1 var(--fd-spl-sans); letter-spacing: -0.04em; }
299.fd-spl-tag { font: 500 12px/1 var(--fd-spl-mono); letter-spacing: 0.02em; transition: opacity var(--fd-dur-2); }
300.fd-spl-login { font: 500 15px/1 var(--fd-spl-sans); padding: 10px 6px; }
301.fd-spl-login:hover { text-decoration: underline; text-underline-offset: 4px; }
302.fd-spl-open { font: 600 14px/1 var(--fd-spl-sans); padding: 12px 18px; border-radius: 9999px; background: var(--fd-spl-save-ink); color: var(--fd-spl-save); transition: transform var(--fd-dur-2) var(--fd-spring); }
303.fd-spl-open:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); }
304
305.fd-spl-body { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 28px; min-width: 0; }
306.fd-spl-head { appearance: none; cursor: default; display: flex; flex-direction: column; align-items: flex-start; gap: 14px; text-align: left; color: inherit; background: none; border: 0; padding: 0; width: 100%; }
307.fd-spl-idx { font: 500 12px/1 var(--fd-spl-mono); letter-spacing: 0.06em; text-transform: uppercase; }
308.fd-spl-word { display: block; white-space: nowrap; }
309.fd-spl-word-spend { font: 900 clamp(4.5rem, 25.5cqw, 15rem)/0.8 var(--fd-spl-sans); letter-spacing: -0.065em; margin-left: -0.05em; }
310.fd-spl-word-save { font: italic 400 clamp(4.5rem, 34cqw, 17rem)/0.8 var(--fd-spl-serif); letter-spacing: -0.045em; margin-left: -0.03em; color: var(--fd-spl-save-ink); }
311
312.fd-spl-detail { display: grid; grid-template-rows: 1fr; max-width: 520px; margin-bottom: 0; transition: grid-template-rows var(--fd-dur-4) var(--fd-ease-out), opacity var(--fd-dur-3) var(--fd-ease-out), margin var(--fd-dur-4) var(--fd-ease-out); }
313.fd-spl-detail-in { display: flex; flex-direction: column; gap: 22px; min-height: 0; overflow: hidden; }
314.fd-spl-half.is-shut .fd-spl-detail { grid-template-rows: 0fr; opacity: 0; margin-bottom: -28px; pointer-events: none; }
315.fd-spl-half.is-shut .fd-spl-tag { opacity: 0; }
316.fd-spl-lede { font: 500 clamp(1.125rem, 1.6vw, 1.375rem)/1.3 var(--fd-spl-sans); letter-spacing: -0.012em; max-width: 30ch; }
317.fd-spl-save .fd-spl-lede { font: 400 clamp(1.25rem, 1.8vw, 1.5rem)/1.3 var(--fd-spl-serif); }
318
319.fd-spl-feed { display: flex; flex-direction: column; border-top: 2px solid var(--fd-spl-spend-ink); }
320.fd-spl-txn { display: grid; grid-template-columns: 34px 1fr auto; align-items: center; gap: 14px; padding: 11px 0; border-bottom: 1px solid rgba(26,15,10,0.28); }
321.fd-spl-glyph { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: var(--fd-spl-spend-ink); color: var(--fd-spl-spend); font: 800 14px/1 var(--fd-spl-sans); }
322.fd-spl-txn-m, .fd-spl-txn-a { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
323.fd-spl-txn-a { text-align: right; align-items: flex-end; }
324.fd-spl-txn b { font: 600 15px/1.2 var(--fd-spl-sans); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
325.fd-spl-txn-a b { font: 600 15px/1.2 var(--fd-spl-mono); }
326.fd-spl-txn span span, .fd-spl-txn-m > span, .fd-spl-txn-a > span { font: 400 12px/1.2 var(--fd-spl-mono); opacity: 0.78; white-space: nowrap; }
327
328.fd-spl-pot { padding: 16px 0 0; border-top: 1px solid rgba(241,232,211,0.4); }
329.fd-spl-pot-row { display: flex; justify-content: space-between; gap: 12px; }
330.fd-spl-pot-name { font: 600 15px/1.2 var(--fd-spl-sans); }
331.fd-spl-pot-due { font: 400 12px/1.4 var(--fd-spl-mono); opacity: 0.8; }
332.fd-spl-bar { position: relative; display: block; height: 10px; margin: 12px 0 10px; border-radius: 9999px; background: rgba(241,232,211,0.16); overflow: hidden; }
333.fd-spl-bar span { position: absolute; left: 0; top: 0; bottom: 0; display: block; border-radius: 9999px; background: var(--fd-spl-brass); }
334.fd-spl-pot-nums { font: 400 13px/1.2 var(--fd-spl-mono); }
335.fd-spl-bal { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding-top: 16px; border-top: 1px solid rgba(241,232,211,0.4); }
336.fd-spl-bal-l { font: 400 12px/1.3 var(--fd-spl-mono); opacity: 0.8; }
337.fd-spl-bal-v { font: 400 40px/1 var(--fd-spl-serif); letter-spacing: -0.025em; font-variant-numeric: tabular-nums; color: var(--fd-spl-brass); }
338
339.fd-spl-foot { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
340.fd-spl-cta {
341 display: inline-flex; align-items: center; gap: 12px; height: 54px; padding: 0 26px; border-radius: 9999px; white-space: nowrap;
342 font: 700 16px/1 var(--fd-spl-sans); transition: transform var(--fd-dur-2) var(--fd-spring), background var(--fd-dur-1);
343}
344.fd-spl-cta svg { display: block; transition: transform var(--fd-dur-2) var(--fd-ease-out); }
345.fd-spl-cta:hover svg { transform: translateX(4px); }
346.fd-spl-cta:active { transform: scale(0.96); transition-duration: var(--fd-dur-1); }
347.fd-spl-cta-spend { background: var(--fd-spl-spend-ink); color: var(--fd-spl-spend); }
348.fd-spl-cta-save { background: var(--fd-spl-brass); color: var(--fd-spl-save); }
349.fd-spl-fine { font: 400 12px/1.4 var(--fd-spl-mono); max-width: 34ch; transition: opacity var(--fd-dur-2); }
350.fd-spl-half.is-shut .fd-spl-fine { opacity: 0; }
351
352.fd-spl-seal {
353 position: absolute; top: 50%; right: -50px; width: 100px; height: 100px; margin-top: -50px; z-index: 5; pointer-events: none;
354 color: var(--fd-spl-spend);
355}
356.fd-spl-seal svg { display: block; width: 100%; height: 100%; animation: fd-spl-spin 24s linear infinite; }
357.fd-spl-seal-t { font: 600 9.2px/1 ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: 0.16em; fill: #FFD9C9; }
358.fd-spl-seal-or { font: italic 400 30px/1 "Iowan Old Style", Palatino, Georgia, serif; fill: #E1B85C; }
359@keyframes fd-spl-spin { to { transform: rotate(360deg); } }
360
361.fd-spl :focus-visible { outline: 2px solid currentColor; outline-offset: 3px; border-radius: 6px; }
362.fd-spl-cta:focus-visible { outline-offset: 4px; border-radius: 9999px; }
363
364@media (max-width: 760px) {
365 .fd-spl { flex-direction: column; min-height: 844px; }
366 .fd-spl-half, .fd-spl.is-spend .fd-spl-spend, .fd-spl.is-save .fd-spl-save { flex: 0 0 auto; transition: none; }
367 .fd-spl .fd-spl-half.is-open { flex: 1 0 auto; }
368 .fd-spl-half.is-shut { cursor: pointer; }
369 .fd-spl-clip { padding: 0 16px 28px; }
370 .fd-spl-top { height: 64px; }
371 .fd-spl-tag { display: none; }
372 .fd-spl-save .fd-spl-top { display: none; }
373 .fd-spl-save .fd-spl-clip { padding-top: 40px; }
374 .fd-spl-logo span { font-size: 22px; }
375 .fd-spl-body { gap: 18px; justify-content: flex-end; }
376 .fd-spl-spend.is-shut .fd-spl-clip { padding-bottom: 44px; }
377 .fd-spl-head { cursor: pointer; flex-direction: row; align-items: flex-end; justify-content: space-between; }
378 .fd-spl-head .fd-spl-idx { display: none; }
379 .fd-spl-word-spend { font-size: 6rem; }
380 .fd-spl-word-save { font-size: 6.4rem; }
381 .fd-spl-detail { transition: opacity var(--fd-dur-3) var(--fd-ease-out); }
382 .fd-spl-half.is-shut .fd-spl-detail, .fd-spl-half.is-shut .fd-spl-fine, .fd-spl-half.is-shut .fd-spl-idx { display: none; }
383 .fd-spl-half.is-shut .fd-spl-body { flex-direction: row; align-items: center; justify-content: space-between; gap: 12px; }
384 .fd-spl-half.is-shut .fd-spl-head { width: auto; }
385 .fd-spl-half.is-shut .fd-spl-word-spend { font-size: 3.6rem; }
386 .fd-spl-half.is-shut .fd-spl-word-save { font-size: 3.9rem; }
387 .fd-spl-half.is-shut .fd-spl-cta { height: 44px; padding: 0 18px; font-size: 14px; }
388 .fd-spl-half.is-open .fd-spl-detail { animation: fd-spl-in var(--fd-dur-3) var(--fd-ease-out) both; }
389 .fd-spl-lede, .fd-spl-save .fd-spl-lede { font-size: 1.0625rem; }
390 .fd-spl-txn:nth-child(3) { display: none; }
391 .fd-spl-bal-v { font-size: 32px; }
392 .fd-spl-cta { height: 50px; }
393 .fd-spl-foot { gap: 12px; }
394 .fd-spl-seal { top: auto; bottom: -30px; right: 16px; margin-top: 0; width: 60px; height: 60px; }
395 .fd-spl-seal-t { display: none; }
396}
397@keyframes fd-spl-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
398@media (prefers-reduced-motion: reduce) {
399 .fd-spl-half, .fd-spl-detail, .fd-spl-tag, .fd-spl-fine, .fd-spl-cta, .fd-spl-cta svg, .fd-spl-open { transition: none; }
400 .fd-spl-seal svg { animation: none; }
401 .fd-spl-half.is-open .fd-spl-detail { animation: none; }
402}
403`;
404
405export function Demo() {
406 return <HeroSplitScreen />;
407}

More Heroes. Same file contract.

Ledger Card Hero, heroes preview: Fintech hero section with a CSS 3D metal card that tilts to the cursor, then flips and docks on scroll as…

Ledger Card Hero

Fintech hero section with a CSS 3D metal card that tilts to the cursor, then flips and docks on scroll as stats count up.

Agent Terminal Hero, heroes preview: AI hero section with a terminal that types a prompt and streams an agent's plan while a dashboard assembles…

Agent Terminal Hero

AI hero section with a terminal that types a prompt and streams an agent's plan while a dashboard assembles beside it.

Maison Editorial Hero, heroes preview: Editorial fashion hero section: a giant serif masthead over three campaign photos, revealed by clip-path wipes

Maison Editorial Hero

Editorial fashion hero section: a giant serif masthead over three campaign photos, revealed by clip-path wipes.

Questions about Spend / Save Split Hero

Run npx shadcn@latest add https://freedesignmd.com/r/hero-split-screen.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.

Split screen hero section: two colour fields that spring open on hover or tap, each with its own type, product and CTA. It is a free, MIT-licensed React heroes component from the freedesignmd library.

Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.

All evergreen browsers (container query units for the giant words). Halves stack and open on tap below 760px.

Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.