Input Field
A floating-label input with helper, error and success states, adornments, counter, clear and password reveal.
A three-step onboarding flow with progress, validation, animated transitions and a review step.
Browser support · All evergreen browsers. Uses ResizeObserver, :has() (focus ring on radio cards) and color-mix().
npx shadcn@latest add https://freedesignmd.com/r/multi-step-signup.json1/**2 * MultiStepSignup — three-step onboarding form (Account → Workspace → Review)3 * with a progress stepper, per-step validation, direction-aware step4 * transitions, a measured height animation and a review step with edit links.5 *6 * Props: onComplete(data) (may return a Promise), productDomain (suffix shown7 * after the workspace URL, default "ledgerline.app"), theme.8 * Exports: MultiStepSignup, SignupData.9 *10 * Browser support: all evergreen browsers (ResizeObserver for height).11 * Accessibility: a <form> per step (Enter submits), labelled fields with12 * aria-invalid + aria-describedby, errors announced via role="alert", focus13 * moves to the step heading on every transition, the stepper is an <ol> with14 * aria-current="step", completed steps are buttons. Radio cards use native15 * radios. Reduced motion: transitions are replaced by a short fade.16 */17import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";18import type { ReactNode } from "react";19 20export interface SignupData {21 name: string;22 email: string;23 password: string;24 company: string;25 size: string;26 role: string;27 slug: string;28 terms: boolean;29}30 31const STEPS = ["Account", "Workspace", "Review"] as const;32const SIZES = ["1–10", "11–50", "51–200", "200+"];33const ROLES = ["Finance lead", "RevOps manager", "Founder / CEO", "Engineer", "Other"];34const FREE = /@(gmail|yahoo|hotmail|outlook|icloud|proton)\./i;35const TAKEN = ["acme", "demo", "test", "admin", "app", "ledgerline"];36const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24);37 38type Errors = Partial<Record<keyof SignupData, string>>;39 40function validate(step: number, d: SignupData): Errors {41 const e: Errors = {};42 if (step === 0) {43 if (d.name.trim().split(/\s+/).length < 2) e.name = "Enter your first and last name";44 if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(d.email)) e.email = "Enter a valid email address";45 else if (FREE.test(d.email)) e.email = "Please use your work email, not a personal one";46 if (d.password.length < 8) e.password = "At least 8 characters";47 else if (!/\d/.test(d.password) || !/[a-z]/i.test(d.password)) e.password = "Include a letter and a number";48 }49 if (step === 1) {50 if (!d.company.trim()) e.company = "Tell us your company name";51 if (!d.size) e.size = "Pick your team size";52 if (!d.role) e.role = "Choose the closest role";53 if (d.slug.length < 3) e.slug = "At least 3 characters";54 else if (TAKEN.includes(d.slug)) e.slug = `“${d.slug}” is taken. Try ${d.slug}-hq`;55 }56 if (step === 2 && !d.terms) e.terms = "Accept the terms to continue";57 return e;58}59 60function FieldWrap({ id, label, error, hint, children }: { id: string; label: string; error?: string; hint?: ReactNode; children: ReactNode }) {61 return (62 <div className="fd-ms-field" data-bad={error ? "" : undefined}>63 <label htmlFor={id}>{label}</label>64 {children}65 <p className="fd-ms-msg" id={`${id}-m`} role={error ? "alert" : undefined} data-err={error ? "" : undefined}>{error ?? hint}</p>66 </div>67 );68}69 70export interface MultiStepSignupProps {71 onComplete?: (d: SignupData) => void | Promise<unknown>;72 productDomain?: string;73 theme?: "light" | "dark";74 className?: string;75}76 77export function MultiStepSignup({ onComplete, productDomain = "ledgerline.app", theme, className }: MultiStepSignupProps) {78 const uid = useId();79 const [step, setStep] = useState(0);80 const [dir, setDir] = useState<"next" | "prev">("next");81 const [furthest, setFurthest] = useState(0);82 const [d, setD] = useState<SignupData>({ name: "", email: "", password: "", company: "", size: "", role: "", slug: "", terms: false });83 const [slugTouched, setSlugTouched] = useState(false);84 const [shown, setShown] = useState<Record<string, boolean>>({});85 const [pw, setPw] = useState(false);86 const [phase, setPhase] = useState<"form" | "loading" | "done">("form");87 const head = useRef<HTMLHeadingElement>(null);88 const inner = useRef<HTMLDivElement>(null);89 const [h, setH] = useState<number | undefined>();90 const first = useRef(true);91 92 const errors = validate(step, d);93 const err = (k: keyof SignupData) => (shown[k] ? errors[k] : undefined);94 const set = <K extends keyof SignupData>(k: K, v: SignupData[K]) => setD((p) => ({ ...p, [k]: v }));95 96 useLayoutEffect(() => {97 const el = inner.current;98 if (!el || typeof ResizeObserver === "undefined") return;99 const ro = new ResizeObserver(() => setH(el.offsetHeight));100 ro.observe(el);101 setH(el.offsetHeight);102 return () => ro.disconnect();103 }, [step, phase]);104 105 useEffect(() => {106 if (first.current) {107 first.current = false;108 return;109 }110 head.current?.focus({ preventScroll: true });111 }, [step, phase]);112 113 const go = (n: number) => {114 setDir(n > step ? "next" : "prev");115 setShown({});116 setStep(n);117 setFurthest((f) => Math.max(f, n));118 };119 120 const next = async (e: React.FormEvent) => {121 e.preventDefault();122 const errs = validate(step, d);123 const keys = Object.keys(errs) as (keyof SignupData)[];124 if (keys.length) {125 setShown(Object.fromEntries(keys.map((k) => [k, true])));126 requestAnimationFrame(() => document.getElementById(`${uid}-${keys[0]}`)?.focus());127 return;128 }129 if (step < 2) return go(step + 1);130 setPhase("loading");131 try {132 await onComplete?.(d);133 await new Promise((r) => window.setTimeout(r, 1200));134 setPhase("done");135 } catch {136 setPhase("form");137 }138 };139 140 const common = (k: keyof SignupData) => ({ id: `${uid}-${k}`, "aria-invalid": !!err(k), "aria-describedby": `${uid}-${k}-m`, onBlur: () => String(d[k] ?? "").length > 0 && setShown((s) => ({ ...s, [k]: true })) });141 142 const slugOk = d.slug.length >= 3 && !TAKEN.includes(d.slug);143 144 return (145 <div className={`fd-ms${className ? " " + className : ""}`} data-fd-theme={theme}>146 <style>{css}</style>147 148 {phase !== "done" && (149 <ol className="fd-ms-steps" aria-label="Sign-up progress">150 {STEPS.map((s, i) => {151 const state = i < step ? "done" : i === step ? "current" : "todo";152 const body = (153 <>154 <span className="fd-ms-dot" data-state={state}>155 {state === "done" ? (156 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg>157 ) : (158 i + 1159 )}160 </span>161 <span className="fd-ms-name">{s}</span>162 </>163 );164 return (165 <li key={s} className="fd-ms-step" data-state={state} aria-current={i === step ? "step" : undefined}>166 {i <= furthest && i !== step && phase === "form" ? (167 <button type="button" className="fd-ms-stepbtn" onClick={() => go(i)} aria-label={`Go back to ${s}`}>{body}</button>168 ) : (169 <span className="fd-ms-stepbtn">{body}</span>170 )}171 {i < STEPS.length - 1 && <span className="fd-ms-line" aria-hidden="true"><i style={{ transform: `scaleX(${i < step ? 1 : 0})` }} /></span>}172 </li>173 );174 })}175 </ol>176 )}177 178 <div className="fd-ms-viewport" style={{ height: h === undefined ? undefined : h + 16 }}>179 <div ref={inner} className="fd-ms-inner">180 {phase === "done" ? (181 <div className="fd-ms-panel fd-ms-success" key="done" data-dir="next">182 <span className="fd-ms-bigcheck" aria-hidden="true"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg></span>183 <h2 ref={head} tabIndex={-1}>You’re in, {d.name.split(" ")[0]}.</h2>184 <p>Your workspace is ready at <b>{d.slug}.{productDomain}</b>. We sent a confirmation link to {d.email}.</p>185 <button type="button" className="fd-ms-btn" data-v="primary" onClick={() => { setPhase("form"); setStep(0); setFurthest(0); setShown({}); setD({ name: "", email: "", password: "", company: "", size: "", role: "", slug: "", terms: false }); setSlugTouched(false); }}>Open workspace</button>186 </div>187 ) : (188 <form className="fd-ms-panel" key={step} data-dir={dir} onSubmit={next} noValidate aria-labelledby={`${uid}-h`}>189 <header>190 <p className="fd-ms-count">Step {step + 1} of 3</p>191 <h2 id={`${uid}-h`} ref={head} tabIndex={-1}>{step === 0 ? "Create your account" : step === 1 ? "Set up your workspace" : "Review and create"}</h2>192 </header>193 194 {step === 0 && (195 <div className="fd-ms-fields">196 <FieldWrap id={`${uid}-name`} label="Full name" error={err("name")}>197 <input {...common("name")} autoComplete="name" placeholder="Priya Natarajan" value={d.name} onChange={(e) => set("name", e.target.value)} autoFocus />198 </FieldWrap>199 <FieldWrap id={`${uid}-email`} label="Work email" error={err("email")} hint="We’ll never share it.">200 <input {...common("email")} type="email" autoComplete="email" placeholder="priya@northbeam.co" value={d.email} onChange={(e) => set("email", e.target.value)} />201 </FieldWrap>202 <FieldWrap id={`${uid}-password`} label="Password" error={err("password")} hint="8+ characters with a letter and a number.">203 <div className="fd-ms-pw">204 <input {...common("password")} type={pw ? "text" : "password"} autoComplete="new-password" value={d.password} onChange={(e) => set("password", e.target.value)} />205 <button type="button" aria-label={pw ? "Hide password" : "Show password"} aria-pressed={pw} onClick={() => setPw(!pw)}>{pw ? "Hide" : "Show"}</button>206 </div>207 </FieldWrap>208 </div>209 )}210 211 {step === 1 && (212 <div className="fd-ms-fields">213 <FieldWrap id={`${uid}-company`} label="Company name" error={err("company")}>214 <input215 {...common("company")}216 autoComplete="organization"217 placeholder="Northbeam"218 value={d.company}219 onChange={(e) => {220 const v = e.target.value;221 setD((p) => ({ ...p, company: v, slug: slugTouched ? p.slug : slugify(v) }));222 }}223 autoFocus224 />225 </FieldWrap>226 <div className="fd-ms-field" data-bad={err("size") ? "" : undefined}>227 <span className="fd-ms-lab" id={`${uid}-size`}>Team size</span>228 <div className="fd-ms-cards" role="radiogroup" aria-labelledby={`${uid}-size`}>229 {SIZES.map((s) => (230 <label key={s} className="fd-ms-card" data-on={d.size === s ? "" : undefined}>231 <input type="radio" name={`${uid}-sz`} value={s} checked={d.size === s} onChange={() => set("size", s)} />232 <span>{s}</span>233 </label>234 ))}235 </div>236 <p className="fd-ms-msg" role={err("size") ? "alert" : undefined} data-err={err("size") ? "" : undefined}>{err("size")}</p>237 </div>238 <FieldWrap id={`${uid}-role`} label="Your role" error={err("role")}>239 <div className="fd-ms-sel">240 <select {...common("role")} value={d.role} onChange={(e) => set("role", e.target.value)} data-empty={!d.role || undefined}>241 <option value="">Select a role</option>242 {ROLES.map((r) => <option key={r}>{r}</option>)}243 </select>244 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6" /></svg>245 </div>246 </FieldWrap>247 <FieldWrap id={`${uid}-slug`} label="Workspace URL" error={err("slug")} hint={slugOk ? <span className="fd-ms-ok">{d.slug}.{productDomain} is available</span> : "Letters, numbers and dashes."}>248 <div className="fd-ms-url">249 <input {...common("slug")} placeholder="northbeam" value={d.slug} onChange={(e) => { setSlugTouched(true); set("slug", slugify(e.target.value)); }} autoCapitalize="none" spellCheck={false} />250 <span>.{productDomain}</span>251 </div>252 </FieldWrap>253 </div>254 )}255 256 {step === 2 && (257 <div className="fd-ms-review">258 {[259 { t: "Account", go: 0, rows: [["Name", d.name], ["Email", d.email], ["Password", "•".repeat(Math.min(d.password.length, 12))]] },260 { t: "Workspace", go: 1, rows: [["Company", d.company], ["Team size", `${d.size} people`], ["Role", d.role], ["URL", `${d.slug}.${productDomain}`]] },261 ].map((g) => (262 <section key={g.t} className="fd-ms-group">263 <div className="fd-ms-ghead"><h3>{g.t}</h3><button type="button" onClick={() => go(g.go)}>Edit<span className="fd-ms-sr"> {g.t}</span></button></div>264 <dl>{g.rows.map(([k, v]) => <div key={k}><dt>{k}</dt><dd>{v}</dd></div>)}</dl>265 </section>266 ))}267 <label className="fd-ms-terms" data-bad={err("terms") ? "" : undefined}>268 <input id={`${uid}-terms`} type="checkbox" checked={d.terms} onChange={(e) => set("terms", e.target.checked)} aria-invalid={!!err("terms")} />269 <span className="fd-ms-box" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg></span>270 <span>I agree to the Terms of Service and the Data Processing Addendum.</span>271 </label>272 <p className="fd-ms-msg" role={err("terms") ? "alert" : undefined} data-err={err("terms") ? "" : undefined}>{err("terms")}</p>273 </div>274 )}275 276 <footer className="fd-ms-actions">277 {step > 0 ? <button type="button" className="fd-ms-btn" data-v="ghost" onClick={() => go(step - 1)} disabled={phase === "loading"}>Back</button> : <span />}278 <button type="submit" className="fd-ms-btn" data-v="primary" data-loading={phase === "loading" || undefined} aria-busy={phase === "loading"}>279 <span>{step === 2 ? "Create workspace" : "Continue"}</span>280 <svg className="fd-ms-spin" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity=".25" strokeWidth="2.5" /><path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" /></svg>281 </button>282 </footer>283 </form>284 )}285 </div>286 </div>287 </div>288 );289}290 291const css = `292:where(.fd-ms,.fd-ms *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}293:where(.fd-ms,.fd-ms *)::before,:where(.fd-ms,.fd-ms *)::after{box-sizing:border-box}294:where(.fd-ms svg){display:block;flex-shrink:0}295:where(.fd-ms button,.fd-ms input,.fd-ms select){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0;outline:0}296.fd-ms{297 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-ease-in-out:cubic-bezier(.65,0,.35,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;298 --s-bg:#fff;--s-fg:#0B0B0F;--s-mut:#6B6B76;--s-line:rgba(11,11,15,.16);--s-line-h:rgba(11,11,15,.28);--s-soft:rgba(11,11,15,.05);--s-accent:#007AFF;--s-err:#D70015;--s-ok:#0F7D3F;299 width:100%;max-width:440px;300 font:400 15px/1.4 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-.005em;color:var(--s-fg);301}302[data-fd-theme="dark"] .fd-ms,.fd-ms[data-fd-theme="dark"]{--s-bg:#16161B;--s-fg:#F5F5F7;--s-mut:#9A9AA6;--s-line:rgba(255,255,255,.14);--s-line-h:rgba(255,255,255,.26);--s-soft:rgba(255,255,255,.06);--s-accent:#0A84FF;--s-err:#FF6A60;--s-ok:#5BE382}303.fd-ms-steps{display:flex;align-items:center;margin-bottom:28px}304.fd-ms-step{display:flex;align-items:center;flex:none}305.fd-ms-step:not(:last-child){flex:1}306.fd-ms-stepbtn{display:inline-flex;align-items:center;gap:10px;appearance:none;-webkit-appearance:none;border:0;outline:0;background:transparent;cursor:default;font-family:inherit;font-size:13px;font-weight:500;line-height:1;color:var(--s-mut);border-radius:9999px;padding:2px 2px 2px 0}307button.fd-ms-stepbtn{cursor:pointer}308button.fd-ms-stepbtn:focus-visible{outline:2px solid color-mix(in srgb,var(--s-accent) 70%,transparent);outline-offset:3px}309.fd-ms-step[data-state="current"] .fd-ms-stepbtn{color:var(--s-fg)}310.fd-ms-dot{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--s-mut);box-shadow:0 0 0 1px var(--s-line);background:var(--s-bg);311 transition:background-color var(--fd-dur-3) var(--fd-ease-out),color var(--fd-dur-3) var(--fd-ease-out),box-shadow var(--fd-dur-3) var(--fd-ease-out),transform var(--fd-dur-3) var(--fd-spring)}312.fd-ms-dot[data-state="current"]{color:var(--s-accent);box-shadow:0 0 0 1.5px var(--s-accent),0 0 0 5px color-mix(in srgb,var(--s-accent) 16%,transparent)}313.fd-ms-dot[data-state="done"]{background:var(--s-accent);color:#fff;box-shadow:0 0 0 1px var(--s-accent)}314@media (hover:hover){button.fd-ms-stepbtn:hover .fd-ms-dot{transform:scale(1.08)}}315.fd-ms-line{flex:1;height:2px;margin:0 12px;border-radius:2px;background:var(--s-line);overflow:hidden}316.fd-ms-line i{display:block;height:100%;background:var(--s-accent);transform-origin:left;transition:transform var(--fd-dur-3) var(--fd-ease-out)}317.fd-ms-viewport{overflow:hidden;margin:-8px;padding:8px;transition:height var(--fd-dur-3) var(--fd-ease-out)}318.fd-ms-inner{width:100%}319.fd-ms-panel{display:flex;flex-direction:column;gap:24px;animation:fd-ms-next var(--fd-dur-3) var(--fd-ease-out) both}320.fd-ms-panel[data-dir="prev"]{animation-name:fd-ms-prev}321.fd-ms-count{font-size:13px;line-height:1;font-weight:500;color:var(--s-accent)}322.fd-ms-panel h2{margin-top:10px;font-size:26px;font-weight:600;line-height:1.15;letter-spacing:-.025em;outline:0;font-family:ui-sans-serif,-apple-system,"SF Pro Display","Inter",sans-serif}323.fd-ms-fields{display:flex;flex-direction:column;gap:16px}324.fd-ms-field>label,.fd-ms-lab{display:block;margin:0 0 6px 2px;font-size:13px;font-weight:500;line-height:1.2;color:var(--s-mut)}325.fd-ms-field input:not([type="radio"]),.fd-ms-field select{display:block;width:100%;height:48px;padding:0 14px;border-radius:12px;background:var(--s-bg);color:var(--s-fg);font-family:inherit;font-size:15px;line-height:1.2;letter-spacing:inherit;326 box-shadow:0 0 0 1px var(--s-line),0 1px 2px rgba(11,11,15,.04);caret-color:var(--s-accent);transition:box-shadow var(--fd-dur-2) var(--fd-ease-out)}327@media (hover:hover){.fd-ms-field input:not([type="radio"]):hover,.fd-ms-field select:hover{box-shadow:0 0 0 1px var(--s-line-h),0 1px 2px rgba(11,11,15,.04)}}328.fd-ms-field input:not([type="radio"]):focus,.fd-ms-field select:focus,.fd-ms-url:focus-within{box-shadow:0 0 0 1.5px var(--s-accent),0 0 0 5px color-mix(in srgb,var(--s-accent) 18%,transparent)}329.fd-ms-field[data-bad] input:not([type="radio"]),.fd-ms-field[data-bad] select,.fd-ms-field[data-bad] .fd-ms-url{box-shadow:0 0 0 1.5px var(--s-err)}330.fd-ms-field input::placeholder{color:var(--s-mut);opacity:.6}331.fd-ms-msg{min-height:18px;padding:6px 2px 0;font-size:12.5px;line-height:1.3;color:var(--s-mut)}332.fd-ms-msg[data-err]{color:var(--s-err);animation:fd-ms-shake 380ms var(--fd-ease-out)}333.fd-ms-ok{color:var(--s-ok)}334.fd-ms-pw{position:relative}.fd-ms-pw input{padding-right:64px!important}335.fd-ms-pw button{position:absolute;right:8px;top:8px;height:32px;padding:0 10px;border-radius:9999px;cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:1;color:var(--s-mut);transition:background-color var(--fd-dur-1)}336.fd-ms-pw button:hover{background:var(--s-soft);color:var(--s-fg)}.fd-ms-pw button:focus-visible{outline:2px solid color-mix(in srgb,var(--s-accent) 70%,transparent);outline-offset:1px}337.fd-ms-sel{position:relative}.fd-ms-sel select{padding-right:40px;cursor:pointer}.fd-ms-sel select[data-empty]{color:var(--s-mut)}.fd-ms-sel select option{color:#0B0B0F;background:#fff}338.fd-ms-sel svg{position:absolute;right:14px;top:16px;pointer-events:none;color:var(--s-mut)}339.fd-ms-url{display:flex;align-items:center;border-radius:12px;background:var(--s-bg);box-shadow:0 0 0 1px var(--s-line),0 1px 2px rgba(11,11,15,.04);transition:box-shadow var(--fd-dur-2) var(--fd-ease-out)}340.fd-ms-url input{flex:1;min-width:0;box-shadow:none!important;background:transparent!important;padding-right:0!important}341.fd-ms-url span{padding:0 14px 0 2px;font-size:15px;color:var(--s-mut);white-space:nowrap}342.fd-ms-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}343.fd-ms-card{position:relative;display:grid;place-items:center;height:48px;margin:0;border-radius:12px;background:var(--s-bg);box-shadow:0 0 0 1px var(--s-line);cursor:pointer;font-size:14px;font-weight:500;color:var(--s-fg);transition:box-shadow var(--fd-dur-2) var(--fd-ease-out),background-color var(--fd-dur-2) var(--fd-ease-out),transform var(--fd-dur-3) var(--fd-spring)}344.fd-ms-card input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}345.fd-ms-card[data-on]{box-shadow:0 0 0 1.5px var(--s-accent);background:color-mix(in srgb,var(--s-accent) 8%,var(--s-bg));color:var(--s-accent)}346.fd-ms-card:has(input:focus-visible){outline:2px solid color-mix(in srgb,var(--s-accent) 70%,transparent);outline-offset:2px}347.fd-ms-card:active{transform:scale(.96);transition-duration:var(--fd-dur-1)}348.fd-ms-review{display:flex;flex-direction:column;gap:12px}349.fd-ms-group{padding:16px;border-radius:16px;background:var(--s-soft)}350.fd-ms-ghead{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}351.fd-ms-ghead h3{font-size:13px;font-weight:600;line-height:1.2;color:var(--s-mut);text-transform:uppercase;letter-spacing:.05em}352.fd-ms-ghead button{cursor:pointer;font-family:inherit;font-size:14px;font-weight:500;line-height:1;color:var(--s-accent);border-radius:6px;padding:2px 4px}353.fd-ms-ghead button:focus-visible{outline:2px solid color-mix(in srgb,var(--s-accent) 70%,transparent);outline-offset:2px}354.fd-ms-group dl{display:flex;flex-direction:column;gap:8px}355.fd-ms-group dl div{display:flex;justify-content:space-between;gap:16px;font-size:14px;line-height:1.3}356.fd-ms-group dt{color:var(--s-mut)}.fd-ms-group dd{text-align:right;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}357.fd-ms-terms{position:relative;display:flex;gap:12px;align-items:flex-start;margin-top:4px;cursor:pointer;font-size:14px;line-height:1.4;color:var(--s-fg)}358.fd-ms-terms input{position:absolute;opacity:0;width:20px;height:20px}359.fd-ms-box{display:grid;place-items:center;flex:none;width:20px;height:20px;margin-top:1px;border-radius:6px;background:var(--s-bg);box-shadow:0 0 0 1px var(--s-line-h);color:transparent;transition:background-color var(--fd-dur-2) var(--fd-ease-out),box-shadow var(--fd-dur-2) var(--fd-ease-out),transform var(--fd-dur-3) var(--fd-spring)}360.fd-ms-terms input:checked+.fd-ms-box{background:var(--s-accent);box-shadow:0 0 0 1px var(--s-accent);color:#fff;transform:scale(1.06)}361.fd-ms-terms input:focus-visible+.fd-ms-box{outline:2px solid color-mix(in srgb,var(--s-accent) 70%,transparent);outline-offset:2px}362.fd-ms-terms[data-bad] .fd-ms-box{box-shadow:0 0 0 1.5px var(--s-err)}363.fd-ms-actions{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:4px}364.fd-ms-btn{position:relative;display:inline-grid;place-items:center;height:48px;padding:0 26px;border-radius:9999px;cursor:pointer;font-family:inherit;font-size:15px;font-weight:500;line-height:1;letter-spacing:-.005em;365 transform:scale(1);transition:transform var(--fd-dur-3) var(--fd-spring),background-color var(--fd-dur-2) var(--fd-ease-out),filter var(--fd-dur-1)}366.fd-ms-btn>*{grid-area:1/1;transition:opacity var(--fd-dur-2) var(--fd-ease-out),transform var(--fd-dur-3) var(--fd-ease-out)}367.fd-ms-btn[data-v="primary"]{background:var(--s-accent);color:#fff;min-width:150px;box-shadow:0 1px 0 rgba(255,255,255,.22) inset,0 1px 2px rgba(0,40,120,.35),0 10px 22px -12px var(--s-accent)}368.fd-ms-btn[data-v="ghost"]{color:var(--s-mut);padding:0 16px}369@media (hover:hover){.fd-ms-btn[data-v="primary"]:hover{filter:brightness(1.08)}.fd-ms-btn[data-v="ghost"]:hover{color:var(--s-fg);background:var(--s-soft)}}370.fd-ms-btn:active{transform:scale(.96);transition:transform var(--fd-dur-1) var(--fd-ease-out)}371.fd-ms-btn:focus-visible{outline:2px solid color-mix(in srgb,var(--s-accent) 70%,transparent);outline-offset:3px}372.fd-ms-btn:disabled{opacity:.5;cursor:not-allowed}373.fd-ms-spin{opacity:0;transform:scale(.7);animation:fd-ms-rot .8s linear infinite}374.fd-ms-btn[data-loading]>span{opacity:0;transform:translateY(-8px)}.fd-ms-btn[data-loading] .fd-ms-spin{opacity:1;transform:none}375.fd-ms-success{align-items:flex-start;padding-top:8px}376.fd-ms-success p{font-size:15px;line-height:1.5;color:var(--s-mut);max-width:38ch}.fd-ms-success b{color:var(--s-fg);font-weight:600}377.fd-ms-bigcheck{display:grid;place-items:center;width:64px;height:64px;border-radius:20px;background:color-mix(in srgb,var(--s-ok) 16%,transparent);color:var(--s-ok);animation:fd-ms-pop 600ms var(--fd-spring)}378.fd-ms-bigcheck path{stroke-dasharray:24;stroke-dashoffset:24;animation:fd-ms-draw 460ms var(--fd-ease-out) 160ms forwards}379.fd-ms-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}380@keyframes fd-ms-next{from{opacity:0;transform:translateX(28px)}}381@keyframes fd-ms-prev{from{opacity:0;transform:translateX(-28px)}}382@keyframes fd-ms-rot{to{transform:rotate(360deg)}}383@keyframes fd-ms-draw{to{stroke-dashoffset:0}}384@keyframes fd-ms-pop{from{transform:scale(.5);opacity:0}}385@keyframes fd-ms-shake{20%{transform:translateX(-3px)}40%{transform:translateX(3px)}60%{transform:translateX(-2px)}80%{transform:translateX(2px)}}386@media (max-width:420px){.fd-ms-name{display:none}.fd-ms-cards{grid-template-columns:repeat(2,1fr)}.fd-ms-step[data-state="current"] .fd-ms-name{display:inline}}387@media (prefers-reduced-motion:reduce){.fd-ms-panel,.fd-ms-panel[data-dir="prev"]{animation:fd-ms-fade 100ms linear both}.fd-ms-viewport,.fd-ms-line i,.fd-ms-dot,.fd-ms-btn,.fd-ms-btn>*{transition:none}.fd-ms-msg[data-err],.fd-ms-bigcheck,.fd-ms-spin{animation:none}.fd-ms-bigcheck path{animation:none;stroke-dashoffset:0}}388@keyframes fd-ms-fade{from{opacity:0}}389@media (prefers-contrast:more){.fd-ms-field input:not([type="radio"]),.fd-ms-field select{box-shadow:0 0 0 1px currentColor}}390`;391 392/* ───────────────────────────── demo ───────────────────────────── */393 394export function Demo() {395 const [theme, setTheme] = useState<"light" | "dark">("light");396 return (397 <div className="fd-ms-demo" data-fd-theme={theme}>398 <style>{demoCss}</style>399 <aside className="fd-ms-demo-side" aria-hidden="true">400 <div className="fd-ms-demo-logo"><i />Ledgerline</div>401 <div className="fd-ms-demo-quote">402 <p>“We closed our books in 3 days instead of 11. Ledgerline replaced four spreadsheets and a lot of Slack pings.”</p>403 <div><b>Marta Vasquez</b><span>VP Finance, Northbeam Logistics</span></div>404 </div>405 <ul className="fd-ms-demo-stats">406 <li><b>3.2×</b><span>faster month-end close</span></li>407 <li><b>1,400</b><span>finance teams onboarded</span></li>408 <li><b>SOC 2</b><span>Type II certified</span></li>409 </ul>410 </aside>411 <main className="fd-ms-demo-main">412 <div className="fd-ms-demo-top">413 <span className="fd-ms-demo-mobilelogo"><i />Ledgerline</span>414 <span className="fd-ms-demo-have">Have an account? <a href="#signin">Sign in</a></span>415 <button type="button" className="fd-ms-demo-theme" aria-label={theme === "dark" ? "Switch to light theme" : "Switch to dark theme"} onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>{theme === "dark" ? "Light" : "Dark"}</button>416 </div>417 <div className="fd-ms-demo-form"><MultiStepSignup theme={theme} /></div>418 </main>419 </div>420 );421}422 423const demoCss = `424:where(.fd-ms-demo,.fd-ms-demo *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}425:where(.fd-ms-demo,.fd-ms-demo *)::before,:where(.fd-ms-demo,.fd-ms-demo *)::after{box-sizing:border-box}426:where(.fd-ms-demo button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}427.fd-ms-demo{--d-font:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;--d-disp:ui-sans-serif,-apple-system,"SF Pro Display","Inter",sans-serif;428 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-dur-2:240ms;429 --d-bg:#fff;--d-ink:#0B0B0F;--d-mut:#6B6B76;--d-line:rgba(11,11,15,.08);--d-accent:#007AFF;430 display:grid;grid-template-columns:minmax(0,5fr) minmax(0,6fr);min-height:100vh;background:var(--d-bg);color:var(--d-ink);font:400 15px/1.45 var(--d-font);-webkit-font-smoothing:antialiased;transition:background-color var(--fd-dur-2) var(--fd-ease-out)}431.fd-ms-demo[data-fd-theme="dark"]{--d-bg:#09090B;--d-ink:#F5F5F7;--d-mut:#9A9AA6;--d-line:rgba(255,255,255,.09);--d-accent:#0A84FF}432.fd-ms-demo-side{display:flex;flex-direction:column;justify-content:space-between;gap:48px;padding:40px clamp(32px,4vw,64px);background:#0B1B34;color:#fff}433.fd-ms-demo-logo,.fd-ms-demo-mobilelogo{display:flex;align-items:center;gap:10px;font:700 18px/1 var(--d-disp);letter-spacing:-.03em}434.fd-ms-demo-logo i,.fd-ms-demo-mobilelogo i{display:block;width:20px;height:20px;border-radius:6px;background:#4DA3FF;position:relative}435.fd-ms-demo-logo i::after,.fd-ms-demo-mobilelogo i::after{content:"";position:absolute;left:5px;right:5px;bottom:5px;height:3px;border-radius:2px;background:#0B1B34;box-shadow:0 -5px 0 #0B1B34,0 -10px 0 #0B1B34}436.fd-ms-demo-quote p{font:500 clamp(20px,2vw,26px)/1.35 var(--d-disp);letter-spacing:-.02em;max-width:22em}437.fd-ms-demo-quote div{display:flex;flex-direction:column;gap:4px;margin-top:24px;font:400 14px/1.3 var(--d-font);color:rgba(255,255,255,.65)}.fd-ms-demo-quote b{font-weight:600;color:#fff}438.fd-ms-demo-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;padding-top:28px;border-top:1px solid rgba(255,255,255,.14)}439.fd-ms-demo-stats li{display:flex;flex-direction:column;gap:6px}.fd-ms-demo-stats b{font:600 24px/1 var(--d-disp);letter-spacing:-.03em;font-variant-numeric:tabular-nums}.fd-ms-demo-stats span{font:400 13px/1.3 var(--d-font);color:rgba(255,255,255,.65)}440.fd-ms-demo-main{display:flex;flex-direction:column;padding:24px clamp(16px,4vw,56px) 56px}441.fd-ms-demo-top{display:flex;align-items:center;justify-content:flex-end;gap:16px}442.fd-ms-demo-mobilelogo{display:none;margin-right:auto}.fd-ms-demo-mobilelogo i{background:var(--d-accent)}.fd-ms-demo-mobilelogo i::after{background:var(--d-bg)}443.fd-ms-demo-have{font:400 14px/1 var(--d-font);color:var(--d-mut)}.fd-ms-demo-have a{font-weight:500;color:var(--d-accent)}444.fd-ms-demo-have a:focus-visible,.fd-ms-demo-theme:focus-visible{outline:2px solid rgba(10,132,255,.7);outline-offset:2px;border-radius:4px}445.fd-ms-demo-theme{cursor:pointer;height:32px;padding:0 14px;border-radius:9999px;box-shadow:0 0 0 1px var(--d-line);font:500 13px/1 var(--d-font);color:var(--d-mut)}446.fd-ms-demo-theme:hover{color:var(--d-ink)}447.fd-ms-demo-form{flex:1;display:flex;align-items:center;justify-content:center;padding-top:32px}448.fd-ms-demo-form>.fd-ms{margin-top:-24px}449@media (max-width:900px){.fd-ms-demo{grid-template-columns:1fr}.fd-ms-demo-side{display:none}.fd-ms-demo-mobilelogo{display:flex}.fd-ms-demo-have{display:none}.fd-ms-demo-form{align-items:flex-start;padding-top:24px}.fd-ms-demo-top{padding-bottom:8px}}450`;A floating-label input with helper, error and success states, adornments, counter, clear and password reveal.
Six-digit code entry with auto-advance, paste and SMS autofill, error shake and a success cascade.
Card entry with brand detection, live formatting, Luhn validation and a preview that flips to the CVC.