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
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 flat3 * colour fields, "Spend" and "Save", each with its own type voice, product4 * fragment and call to action. Hovering, focusing or tapping a half springs5 * 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 the9 * 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 the13 * giant words; fixed sizes otherwise).14 * Motion: flex-grow with the library spring curve (overshoot), word size15 * 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 opens18 * 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 <section122 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 <div130 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 <button149 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">&</text>198 </svg>199 </div>200 </div>201 202 {/* SAVE */}203 <div204 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 <button217 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.
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.


