Newsletter Inline

Single-line email capture with inline validation, a loading button and a success morph with undo.

  • Forms
  • Food & beverage
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Uses the inert attribute (Chrome 102+, Safari 15.5+, Firefox 112+).

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/newsletter-inline.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/forms/newsletter-inline.tsx
1/**
2 * NewsletterInline — a single-line email capture with inline validation,
3 * a loading state inside the button and a success morph (the field turns into
4 * a confirmation with a drawn check, with an undo link).
5 *
6 * Props: onSubmit(email) (return a Promise; throw Error("message") to show a
7 * server error such as "already subscribed"), placeholder, cta, successTitle,
8 * successBody, size ("md" | "lg"), theme.
9 *
10 * Browser support: all evergreen browsers.
11 * Accessibility: <form> with a labelled email input (visually hidden label),
12 * aria-invalid + aria-describedby, errors in role="alert", the success message
13 * is a polite live region and receives focus management through the "Use a
14 * different email" button. Validation runs on blur (if non-empty) and submit;
15 * once an error is shown it clears as soon as the value becomes valid.
16 * Reduced motion: no shake, the morph is an opacity fade.
17 */
18import { useEffect, useId, useRef, useState } from "react";
19
20export interface NewsletterInlineProps {
21 onSubmit?: (email: string) => void | Promise<unknown>;
22 placeholder?: string;
23 cta?: string;
24 successTitle?: string;
25 successBody?: (email: string) => string;
26 size?: "md" | "lg";
27 theme?: "light" | "dark";
28 className?: string;
29}
30
31const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
32
33export function NewsletterInline({
34 onSubmit,
35 placeholder = "Your email address",
36 cta = "Subscribe",
37 successTitle = "You’re on the list",
38 successBody = (e) => `We sent a confirmation link to ${e}.`,
39 size = "lg",
40 theme,
41 className,
42}: NewsletterInlineProps) {
43 const uid = useId();
44 const [email, setEmail] = useState("");
45 const [error, setError] = useState<string | undefined>();
46 const [phase, setPhase] = useState<"idle" | "loading" | "done">("idle");
47 const [shake, setShake] = useState(0);
48 const input = useRef<HTMLInputElement>(null);
49 const alive = useRef(true);
50 useEffect(() => () => void (alive.current = false), []);
51
52 const check = (v: string) => (!v.trim() ? "Add your email to subscribe" : EMAIL.test(v.trim()) ? undefined : "That doesn’t look like an email. Try name@domain.com");
53
54 const submit = async (e: React.FormEvent) => {
55 e.preventDefault();
56 if (phase !== "idle") return;
57 const err = check(email);
58 if (err) {
59 setError(err);
60 setShake((s) => s + 1);
61 input.current?.focus();
62 return;
63 }
64 setError(undefined);
65 setPhase("loading");
66 try {
67 await onSubmit?.(email.trim());
68 await new Promise((r) => window.setTimeout(r, 900));
69 if (alive.current) setPhase("done");
70 } catch (ex) {
71 if (!alive.current) return;
72 setPhase("idle");
73 setError(ex instanceof Error ? ex.message : "Something went wrong. Please try again.");
74 setShake((s) => s + 1);
75 input.current?.focus();
76 }
77 };
78
79 return (
80 <div className={`fd-nl${className ? " " + className : ""}`} data-size={size} data-phase={phase} data-bad={error ? "" : undefined} data-fd-theme={theme}>
81 <style>{css}</style>
82 <div key={shake} className="fd-nl-box" data-shake={shake && error ? "" : undefined}>
83 <form className="fd-nl-form" onSubmit={submit} noValidate aria-hidden={phase === "done"} inert={phase === "done"}>
84 <label htmlFor={`${uid}-e`} className="fd-nl-sr">Email address</label>
85 <svg className="fd-nl-mail" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="3" /><path d="M3.5 7.5l8.5 6 8.5-6" /></svg>
86 <input
87 ref={input}
88 id={`${uid}-e`}
89 type="email"
90 inputMode="email"
91 autoComplete="email"
92 placeholder={placeholder}
93 value={email}
94 disabled={phase === "loading"}
95 aria-invalid={!!error}
96 aria-describedby={`${uid}-m`}
97 onChange={(e) => {
98 setEmail(e.target.value);
99 if (error && !check(e.target.value)) setError(undefined);
100 }}
101 onBlur={() => email && setError(check(email))}
102 />
103 <button type="submit" className="fd-nl-btn" aria-busy={phase === "loading"}>
104 <span>{cta}</span>
105 <svg className="fd-nl-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=".3" strokeWidth="2.5" /><path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" /></svg>
106 </button>
107 </form>
108
109 <div className="fd-nl-done" role="status" aria-live="polite">
110 {phase === "done" && (
111 <>
112 <span className="fd-nl-badge" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.8" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg></span>
113 <span className="fd-nl-dtxt"><b>{successTitle}</b><span>{successBody(email.trim())}</span></span>
114 <button type="button" className="fd-nl-undo" onClick={() => { setPhase("idle"); setEmail(""); requestAnimationFrame(() => input.current?.focus()); }}>Use a different email</button>
115 </>
116 )}
117 </div>
118 </div>
119 <p id={`${uid}-m`} className="fd-nl-msg" role={error ? "alert" : undefined} data-err={error ? "" : undefined}>{error}</p>
120 </div>
121 );
122}
123
124const css = `
125:where(.fd-nl,.fd-nl *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
126:where(.fd-nl,.fd-nl *)::before,:where(.fd-nl,.fd-nl *)::after{box-sizing:border-box}
127:where(.fd-nl svg){display:block;flex-shrink:0}
128:where(.fd-nl button,.fd-nl input){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0;outline:0}
129.fd-nl{
130 --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;
131 --n-bg:#fff;--n-fg:#1A1410;--n-mut:#7A6E64;--n-line:rgba(26,20,16,.16);--n-accent:#1A1410;--n-on:#fff;--n-err:#C2271B;--n-ok:#1F7A45;--n-okbg:#E8F4EC;--n-ring:rgba(194,98,28,.55);
132 width:100%;max-width:520px;
133 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(--n-fg);
134}
135[data-fd-theme="dark"] .fd-nl,.fd-nl[data-fd-theme="dark"]{--n-bg:#241C16;--n-fg:#F6EEE6;--n-mut:#A89A8C;--n-line:rgba(246,238,230,.16);--n-accent:#F6EEE6;--n-on:#1A1410;--n-err:#FF8A7A;--n-ok:#7BE0A4;--n-okbg:#16301F;--n-ring:rgba(255,150,70,.6)}
136.fd-nl[data-size="md"]{--n-h:48px}.fd-nl[data-size="lg"]{--n-h:60px}
137.fd-nl-box{position:relative;display:grid;height:var(--n-h);border-radius:9999px;background:var(--n-bg);box-shadow:0 0 0 1px var(--n-line),0 1px 2px rgba(26,20,16,.05),0 12px 24px -16px rgba(26,20,16,.25);
138 transition:box-shadow var(--fd-dur-2) var(--fd-ease-out),background-color var(--fd-dur-3) var(--fd-ease-out)}
139.fd-nl-box:focus-within{box-shadow:0 0 0 1.5px var(--n-accent),0 0 0 5px var(--n-ring)}
140.fd-nl[data-bad] .fd-nl-box{box-shadow:0 0 0 1.5px var(--n-err)}
141.fd-nl[data-bad] .fd-nl-box:focus-within{box-shadow:0 0 0 1.5px var(--n-err),0 0 0 5px color-mix(in srgb,var(--n-err) 22%,transparent)}
142.fd-nl[data-phase="done"] .fd-nl-box{background:var(--n-okbg);box-shadow:0 0 0 1px color-mix(in srgb,var(--n-ok) 40%,transparent)}
143.fd-nl-box[data-shake]{animation:fd-nl-shake 440ms cubic-bezier(.36,.07,.19,.97)}
144.fd-nl-form,.fd-nl-done{grid-area:1/1;display:flex;align-items:center;min-width:0;transition:opacity var(--fd-dur-3) var(--fd-ease-in-out),transform var(--fd-dur-3) var(--fd-ease-out)}
145.fd-nl-form{gap:0;padding:0 calc((var(--n-h) - 44px) / 2) 0 22px}
146.fd-nl[data-size="md"] .fd-nl-form{padding-right:4px}
147.fd-nl[data-phase="done"] .fd-nl-form{opacity:0;transform:translateY(-10px) scale(.98);pointer-events:none}
148.fd-nl-mail{color:var(--n-mut);margin-right:12px}
149.fd-nl-form input{flex:1;min-width:0;height:100%;color:var(--n-fg);font-family:inherit;font-size:16px;line-height:1.2;letter-spacing:inherit;caret-color:var(--n-fg);text-overflow:ellipsis}
150.fd-nl-form input::placeholder{color:var(--n-mut);opacity:.85}
151.fd-nl-form input:disabled{opacity:.6}
152.fd-nl-btn{position:relative;display:grid;place-items:center;flex:none;height:calc(var(--n-h) - 16px);padding:0 22px;border-radius:9999px;cursor:pointer;overflow:hidden;
153 background:var(--n-accent);color:var(--n-on);font-family:inherit;font-size:15px;font-weight:600;line-height:1;letter-spacing:-.005em;
154 transform:scale(1);transition:transform var(--fd-dur-3) var(--fd-spring),filter var(--fd-dur-1)}
155.fd-nl[data-size="lg"] .fd-nl-btn{min-width:116px}
156@media (hover:hover){.fd-nl-btn:hover{filter:brightness(1.18)}[data-fd-theme="dark"] .fd-nl-btn:hover,.fd-nl[data-fd-theme="dark"] .fd-nl-btn:hover{filter:brightness(.92)}}
157.fd-nl-btn:active{transform:scale(.95);transition:transform var(--fd-dur-1) var(--fd-ease-out)}
158.fd-nl-btn:focus-visible{outline:2px solid var(--n-fg);outline-offset:2px}
159.fd-nl-btn>*{grid-area:1/1;transition:opacity var(--fd-dur-2) var(--fd-ease-out),transform var(--fd-dur-3) var(--fd-ease-out)}
160.fd-nl-spin{opacity:0;transform:scale(.6);animation:fd-nl-rot .8s linear infinite}
161.fd-nl[data-phase="loading"] .fd-nl-btn>span{opacity:0;transform:translateY(-10px)}
162.fd-nl[data-phase="loading"] .fd-nl-spin{opacity:1;transform:none}
163.fd-nl-done{gap:14px;padding:0 20px 0 12px;opacity:0;transform:translateY(10px) scale(.98);pointer-events:none}
164.fd-nl[data-phase="done"] .fd-nl-done{opacity:1;transform:none;pointer-events:auto;transition-delay:80ms}
165.fd-nl-badge{display:grid;place-items:center;flex:none;width:36px;height:36px;border-radius:50%;background:var(--n-ok);color:var(--n-okbg);animation:fd-nl-pop 560ms var(--fd-spring)}
166.fd-nl-badge path{stroke-dasharray:24;stroke-dashoffset:24;animation:fd-nl-draw 420ms var(--fd-ease-out) 160ms forwards}
167.fd-nl-dtxt{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;font-size:13px;line-height:1.25;color:var(--n-mut)}
168.fd-nl-dtxt b{font-size:15px;font-weight:600;color:var(--n-fg)}
169.fd-nl-dtxt span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
170.fd-nl-undo{flex:none;cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:1;color:var(--n-ok);padding:8px 6px;border-radius:8px;text-decoration:underline;text-underline-offset:3px}
171.fd-nl-undo:focus-visible{outline:2px solid var(--n-ok);outline-offset:1px}
172.fd-nl-msg{min-height:0;max-height:0;overflow:hidden;padding:0 22px;font-size:13px;line-height:1.35;color:var(--n-err);opacity:0;transform:translateY(-4px);transition:max-height var(--fd-dur-3) var(--fd-ease-out),opacity var(--fd-dur-2),transform var(--fd-dur-2) var(--fd-ease-out),padding var(--fd-dur-2)}
173.fd-nl-msg[data-err]{max-height:48px;opacity:1;transform:none;padding-top:10px}
174.fd-nl-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
175@keyframes fd-nl-rot{to{transform:rotate(360deg)}}
176@keyframes fd-nl-draw{to{stroke-dashoffset:0}}
177@keyframes fd-nl-pop{from{transform:scale(.4);opacity:0}}
178@keyframes fd-nl-shake{10%,90%{transform:translateX(-2px)}20%,80%{transform:translateX(4px)}30%,50%,70%{transform:translateX(-6px)}40%,60%{transform:translateX(6px)}}
179@media (max-width:480px){.fd-nl-mail{display:none}.fd-nl-form{padding-left:18px}.fd-nl-undo{display:none}.fd-nl[data-size="lg"] .fd-nl-btn{min-width:0;padding:0 18px}.fd-nl-done{padding-right:12px}}
180@media (prefers-reduced-motion:reduce){.fd-nl-box[data-shake]{animation:none}.fd-nl-form,.fd-nl-done{transition:opacity 100ms linear;transform:none!important}.fd-nl-badge{animation:none}.fd-nl-badge path{animation:none;stroke-dashoffset:0}.fd-nl-spin{animation:none}.fd-nl-btn,.fd-nl-btn:active{transition:none;transform:none}}
181@media (prefers-contrast:more){.fd-nl-box{box-shadow:0 0 0 1px currentColor}}
182`;
183
184/* ───────────────────────────── demo ───────────────────────────── */
185
186export function Demo() {
187 const [theme, setTheme] = useState<"light" | "dark">("light");
188 return (
189 <div className="fd-nl-demo" data-fd-theme={theme}>
190 <style>{demoCss}</style>
191 <header className="fd-nl-demo-bar">
192 <span className="fd-nl-demo-brand">Marlow<i>&amp;</i>Finch</span>
193 <nav aria-label="Primary"><a href="#shop">Shop</a><a href="#subscriptions">Subscriptions</a><a href="#brew">Brew guides</a><a href="#roastery">Roastery</a></nav>
194 <button type="button" className="fd-nl-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>
195 </header>
196
197 <main className="fd-nl-demo-main">
198 <section className="fd-nl-demo-copy" aria-labelledby="fd-nl-h">
199 <p className="fd-nl-demo-eyebrow">The Roast Letter · every other Thursday</p>
200 <h1 id="fd-nl-h">First pick of every small-lot roast.</h1>
201 <p className="fd-nl-demo-sub">Tasting notes, brew recipes and early access to limited harvests from Huila, Yirgacheffe and Kerinci — straight from our roastery in Portland.</p>
202 <NewsletterInline
203 theme={theme}
204 cta="Join free"
205 placeholder="you@email.com"
206 onSubmit={(email) => new Promise<void>((res, rej) => window.setTimeout(() => (email.toLowerCase() === "taken@example.com" ? rej(new Error("That address is already subscribed. Check your inbox for the latest letter.")) : res()), 700))}
207 successTitle="Welcome to the Roast Letter"
208 successBody={() => "Confirm in your inbox to claim 10% off."}
209 />
210 <ul className="fd-nl-demo-perks">
211 <li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg>10% off your first bag</li>
212 <li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg>Early access to micro-lots</li>
213 <li><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" /></svg>Unsubscribe in one click</li>
214 </ul>
215 <p className="fd-nl-demo-fine">18,400 readers · No spam. Read our <a href="#privacy">privacy policy</a>.</p>
216 </section>
217
218 <div className="fd-nl-demo-art" aria-hidden="true">
219 <svg viewBox="0 0 360 440" width="100%" height="100%">
220 <ellipse cx="180" cy="410" rx="130" ry="14" fill="rgba(26,20,16,.16)" />
221 <path d="M82 96 Q180 70 278 96 L296 400 Q180 424 64 400 Z" fill="#C9A06B" />
222 <path d="M82 96 Q180 70 278 96 L278 120 Q180 96 82 120 Z" fill="#B58B57" />
223 <path d="M82 96 Q180 70 278 96" fill="none" stroke="#A67B48" strokeWidth="3" />
224 <rect x="106" y="190" width="148" height="164" rx="10" fill="#FBF4E8" />
225 <circle cx="180" cy="236" r="26" fill="#1A1410" />
226 <ellipse cx="180" cy="236" rx="9" ry="22" fill="#FBF4E8" transform="rotate(24 180 236)" />
227 <rect x="126" y="282" width="108" height="9" rx="4" fill="#1A1410" />
228 <rect x="140" y="302" width="80" height="6" rx="3" fill="#1A1410" opacity=".5" />
229 <rect x="148" y="318" width="64" height="6" rx="3" fill="#1A1410" opacity=".3" />
230 <path d="M82 96 L74 70 Q180 54 286 70 L278 96" fill="#9C713F" />
231 </svg>
232 <span className="fd-nl-demo-tag">Huila Pink Bourbon<small>Notes of plum, jasmine, brown sugar</small></span>
233 </div>
234 </main>
235 </div>
236 );
237}
238
239const demoCss = `
240:where(.fd-nl-demo,.fd-nl-demo *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
241:where(.fd-nl-demo,.fd-nl-demo *)::before,:where(.fd-nl-demo,.fd-nl-demo *)::after{box-sizing:border-box}
242:where(.fd-nl-demo svg){display:block;flex-shrink:0}
243:where(.fd-nl-demo button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
244.fd-nl-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;
245 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-dur-2:240ms;
246 --d-bg:#F4EBDD;--d-ink:#1A1410;--d-mut:#6F6358;--d-line:rgba(26,20,16,.12);--d-art:#E8D6BA;--d-accent:#B8541C;
247 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)}
248.fd-nl-demo[data-fd-theme="dark"]{--d-bg:#17110D;--d-ink:#F6EEE6;--d-mut:#A89A8C;--d-line:rgba(246,238,230,.12);--d-art:#2A1F17;--d-accent:#FF9A55}
249.fd-nl-demo-bar{display:flex;align-items:center;gap:32px;height:68px;padding:0 clamp(16px,4vw,48px)}
250.fd-nl-demo-brand{margin-right:auto;font:700 20px/1 var(--d-disp);letter-spacing:-.04em}.fd-nl-demo-brand i{font-style:normal;color:var(--d-accent);margin:0 2px}
251.fd-nl-demo-bar nav{display:flex;gap:28px;font:500 14px/1 var(--d-font);color:var(--d-mut)}.fd-nl-demo-bar nav a{padding:8px 0}.fd-nl-demo-bar nav a:hover{color:var(--d-ink)}
252.fd-nl-demo-bar a:focus-visible,.fd-nl-demo-theme:focus-visible,.fd-nl-demo-fine a:focus-visible{outline:2px solid var(--d-accent);outline-offset:2px;border-radius:4px}
253.fd-nl-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)}.fd-nl-demo-theme:hover{color:var(--d-ink)}
254.fd-nl-demo-main{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);gap:clamp(24px,5vw,72px);align-items:center;max-width:1160px;margin:0 auto;padding:clamp(24px,6vh,72px) clamp(16px,4vw,48px) 64px}
255.fd-nl-demo-eyebrow{font:600 12px/1 var(--d-font);letter-spacing:.08em;text-transform:uppercase;color:var(--d-accent)}
256.fd-nl-demo-copy h1{margin:20px 0 0;font:600 clamp(36px,5.2vw,64px)/1.02 var(--d-disp);letter-spacing:-.04em;max-width:11em}
257.fd-nl-demo-sub{margin:20px 0 32px;max-width:30em;font:400 17px/1.55 var(--d-font);letter-spacing:-.01em;color:var(--d-mut)}
258.fd-nl-demo-perks{display:flex;flex-wrap:wrap;gap:8px 20px;margin-top:24px;font:500 14px/1 var(--d-font)}
259.fd-nl-demo-perks li{display:flex;align-items:center;gap:8px}.fd-nl-demo-perks svg{color:var(--d-accent)}
260.fd-nl-demo-fine{margin-top:20px;font:400 13px/1.4 var(--d-font);color:var(--d-mut)}.fd-nl-demo-fine a{text-decoration:underline;text-underline-offset:3px}
261.fd-nl-demo-art{position:relative;aspect-ratio:1/1.15;max-height:560px;border-radius:32px;background:var(--d-art);padding:8% 12% 4%;box-shadow:0 0 0 1px var(--d-line)}
262.fd-nl-demo-tag{position:absolute;left:20px;bottom:20px;display:flex;flex-direction:column;gap:4px;padding:12px 16px;border-radius:14px;background:var(--d-bg);box-shadow:0 0 0 1px var(--d-line),0 8px 24px -12px rgba(0,0,0,.3);font:600 14px/1.2 var(--d-font)}
263.fd-nl-demo-tag small{font:400 12px/1.3 var(--d-font);color:var(--d-mut)}
264@media (max-width:860px){.fd-nl-demo-main{grid-template-columns:1fr}.fd-nl-demo-bar nav{display:none}.fd-nl-demo-art{max-height:380px;aspect-ratio:auto;height:340px;padding:6% 24% 3%}}
265`;

More Forms. Same file contract.

Input Field

A floating-label input with helper, error and success states, adornments, counter, clear and password reveal.

OTP Input

Six-digit code entry with auto-advance, paste and SMS autofill, error shake and a success cascade.

Payment Form

Card entry with brand detection, live formatting, Luhn validation and a preview that flips to the CVC.