Input Field

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

  • Forms
  • Healthcare
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Uses color-mix() for focus halos.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/input-field.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/forms/input-field.tsx
1/**
2 * InputField — text input with a floating label, helper / error / success
3 * states, prefix + suffix adornments, character counter, clear button and a
4 * password reveal toggle. Also supports `multiline` (textarea).
5 *
6 * Props: label, value / defaultValue / onChange, type, helper, error,
7 * success (boolean or message), prefix, suffix, maxLength (shows 12 / 120),
8 * clearable, multiline + rows, placeholder (shown once the label floats),
9 * disabled, readOnly, size ("md" | "lg"), theme, plus native input props.
10 * CSS vars: --fd-in-radius, --fd-in-accent.
11 *
12 * Browser support: all evergreen browsers (uses :has() only for the progressive
13 * hover ring; everything else works without it).
14 * Accessibility: real <label for>, aria-invalid, aria-describedby for helper /
15 * error / counter, error text is announced (role="alert"), the password toggle
16 * is a <button aria-pressed> with a label, the clear button is keyboard
17 * reachable. Reduced motion: label moves without easing.
18 */
19import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
20import type { InputHTMLAttributes, ReactNode } from "react";
21
22type Base = Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "prefix" | "onChange" | "value" | "defaultValue">;
23
24export interface InputFieldProps extends Base {
25 label: string;
26 value?: string;
27 defaultValue?: string;
28 onChange?: (value: string) => void;
29 helper?: ReactNode;
30 error?: ReactNode;
31 success?: boolean | string;
32 prefix?: ReactNode;
33 suffix?: ReactNode;
34 clearable?: boolean;
35 multiline?: boolean;
36 rows?: number;
37 size?: "md" | "lg";
38 theme?: "light" | "dark";
39}
40
41export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(
42 {
43 label,
44 value,
45 defaultValue = "",
46 onChange,
47 type = "text",
48 helper,
49 error,
50 success,
51 prefix,
52 suffix,
53 maxLength,
54 clearable,
55 multiline,
56 rows = 3,
57 size = "md",
58 theme,
59 disabled,
60 readOnly,
61 placeholder,
62 className,
63 id,
64 onFocus,
65 onBlur,
66 ...rest
67 },
68 fwd,
69) {
70 const uid = useId();
71 const fid = id ?? `${uid}-f`;
72 const el = useRef<HTMLInputElement & HTMLTextAreaElement>(null);
73 useImperativeHandle(fwd, () => el.current as HTMLInputElement);
74 const [inner, setInner] = useState(defaultValue);
75 const [focus, setFocus] = useState(false);
76 const [reveal, setReveal] = useState(false);
77 const [autofilled, setAutofilled] = useState(false);
78 const val = value ?? inner;
79 const isPw = type === "password";
80 const floated = focus || val.length > 0 || !!prefix || autofilled || (!!placeholder && false);
81 const state = error ? "error" : success ? "success" : "idle";
82
83 // browsers fill autofill values without firing change on some engines
84 useEffect(() => {
85 const t = window.setTimeout(() => {
86 if (el.current && el.current.value && !val) setAutofilled(true);
87 }, 150);
88 return () => window.clearTimeout(t);
89 }, [val]);
90
91 const set = useCallback(
92 (v: string) => {
93 if (value === undefined) setInner(v);
94 onChange?.(v);
95 },
96 [onChange, value],
97 );
98
99 const hid = `${fid}-help`;
100 const eid = `${fid}-err`;
101 const cid = `${fid}-cnt`;
102 const describedBy = [error ? eid : helper ? hid : null, maxLength ? cid : null].filter(Boolean).join(" ") || undefined;
103 const showClear = clearable && val.length > 0 && !disabled && !readOnly;
104 const successMsg = typeof success === "string" ? success : null;
105
106 const common = {
107 id: fid,
108 ref: el,
109 value: val,
110 disabled,
111 readOnly,
112 maxLength,
113 "aria-invalid": error ? true : undefined,
114 "aria-describedby": describedBy,
115 className: "fd-in-control",
116 placeholder: floated ? placeholder : undefined,
117 onChange: (e: { target: { value: string } }) => {
118 setAutofilled(false);
119 set(e.target.value);
120 },
121 onFocus: (e: React.FocusEvent<HTMLInputElement>) => {
122 setFocus(true);
123 onFocus?.(e);
124 },
125 onBlur: (e: React.FocusEvent<HTMLInputElement>) => {
126 setFocus(false);
127 onBlur?.(e);
128 },
129 };
130
131 return (
132 <div className={`fd-in${className ? " " + className : ""}`} data-size={size} data-state={state} data-focus={focus || undefined} data-floated={floated || undefined} data-disabled={disabled || undefined} data-multiline={multiline || undefined} data-fd-theme={theme}>
133 <style>{css}</style>
134 <div className="fd-in-box">
135 {prefix && <span className="fd-in-fix fd-in-pre">{prefix}</span>}
136 <div className="fd-in-main">
137 {multiline ? (
138 <textarea {...(common as object)} rows={rows} {...(rest as object)} />
139 ) : (
140 <input {...common} type={isPw && reveal ? "text" : type} {...rest} />
141 )}
142 <label className="fd-in-label" htmlFor={fid}>{label}</label>
143 </div>
144 <span className="fd-in-end">
145 {showClear && (
146 <button type="button" className="fd-in-btn" aria-label={`Clear ${label}`} onClick={() => { set(""); el.current?.focus(); }}>
147 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm3.7 12.3a1 1 0 0 1-1.4 1.4L12 13.4l-2.3 2.3a1 1 0 0 1-1.4-1.4l2.3-2.3-2.3-2.3a1 1 0 0 1 1.4-1.4l2.3 2.3 2.3-2.3a1 1 0 0 1 1.4 1.4L13.4 12z" /></svg>
148 </button>
149 )}
150 {isPw && (
151 <button type="button" className="fd-in-btn" aria-label={reveal ? "Hide password" : "Show password"} aria-pressed={reveal} onClick={() => setReveal((r) => !r)}>
152 {reveal ? (
153 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 3l18 18M10.6 6.1A9.8 9.8 0 0 1 12 6c5 0 8.5 4 9.5 6a14 14 0 0 1-2.6 3.4M6.6 7.7A14 14 0 0 0 2.5 12c1 2 4.5 6 9.5 6 1.5 0 2.8-.4 4-.9M9.9 10a3 3 0 0 0 4.1 4.1" /></svg>
154 ) : (
155 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M2.5 12C3.5 10 7 6 12 6s8.5 4 9.5 6c-1 2-4.5 6-9.5 6s-8.5-4-9.5-6z" /><circle cx="12" cy="12" r="3" /></svg>
156 )}
157 </button>
158 )}
159 {suffix && <span className="fd-in-fix fd-in-suf">{suffix}</span>}
160 {state === "success" && (
161 <span className="fd-in-state fd-in-ok" aria-hidden="true">
162 <svg width="18" height="18" 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>
163 </span>
164 )}
165 {state === "error" && (
166 <span className="fd-in-state fd-in-bad" aria-hidden="true">
167 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round"><path d="M12 7v6M12 17h.01" /></svg>
168 </span>
169 )}
170 </span>
171 </div>
172
173 <div className="fd-in-foot">
174 <div className="fd-in-msgwrap" data-open={error || helper || successMsg ? "" : undefined}>
175 <div className="fd-in-msgin">
176 {error ? (
177 <p id={eid} className="fd-in-msg fd-in-err" role="alert">{error}</p>
178 ) : successMsg ? (
179 <p className="fd-in-msg fd-in-okmsg">{successMsg}</p>
180 ) : helper ? (
181 <p id={hid} className="fd-in-msg">{helper}</p>
182 ) : null}
183 </div>
184 </div>
185 {maxLength !== undefined && (
186 <span id={cid} className="fd-in-count" data-near={val.length >= maxLength * 0.9 || undefined} aria-live="off">
187 {val.length} / {maxLength}
188 </span>
189 )}
190 </div>
191 </div>
192 );
193});
194
195const css = `
196:where(.fd-in,.fd-in *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
197:where(.fd-in,.fd-in *)::before,:where(.fd-in,.fd-in *)::after{box-sizing:border-box}
198:where(.fd-in svg){display:block;flex-shrink:0}
199:where(.fd-in button,.fd-in input,.fd-in textarea){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0;outline:0}
200.fd-in{
201 --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;
202 --n-bg:#fff;--n-fg:#0B0B0F;--n-mut:#6B6B76;--n-line:rgba(11,11,15,.16);--n-line-h:rgba(11,11,15,.28);--n-accent:var(--fd-in-accent,#007AFF);--n-err:#D70015;--n-ok:#0F7D3F;--n-dis:#F1F1F4;
203 --n-sh:0 1px 2px rgba(11,11,15,.04);
204 display:block;width:100%;
205 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);
206}
207[data-fd-theme="dark"] .fd-in,.fd-in[data-fd-theme="dark"]{--n-bg:#16161B;--n-fg:#F5F5F7;--n-mut:#9A9AA6;--n-line:rgba(255,255,255,.14);--n-line-h:rgba(255,255,255,.26);--n-accent:var(--fd-in-accent,#0A84FF);--n-err:#FF6A60;--n-ok:#5BE382;--n-dis:#1B1B21;--n-sh:0 1px 0 rgba(255,255,255,.03) inset}
208.fd-in[data-size="md"]{--n-h:52px}.fd-in[data-size="lg"]{--n-h:60px}
209.fd-in-box{position:relative;display:flex;align-items:stretch;min-height:var(--n-h);border-radius:var(--fd-in-radius,14px);background:var(--n-bg);box-shadow:0 0 0 1px var(--n-line),var(--n-sh);
210 transition:box-shadow var(--fd-dur-2) var(--fd-ease-out),background-color var(--fd-dur-2) var(--fd-ease-out)}
211@media (hover:hover){.fd-in:not([data-disabled]):not([data-focus]):not([data-state="error"]):not([data-state="success"]) .fd-in-box:hover{box-shadow:0 0 0 1px var(--n-line-h),var(--n-sh)}}
212.fd-in[data-focus] .fd-in-box{box-shadow:0 0 0 1.5px var(--n-accent),0 0 0 5px color-mix(in srgb,var(--n-accent) 18%,transparent)}
213.fd-in[data-state="error"] .fd-in-box{box-shadow:0 0 0 1.5px var(--n-err)}
214.fd-in[data-state="error"][data-focus] .fd-in-box{box-shadow:0 0 0 1.5px var(--n-err),0 0 0 5px color-mix(in srgb,var(--n-err) 18%,transparent)}
215.fd-in[data-state="success"] .fd-in-box{box-shadow:0 0 0 1.5px var(--n-ok)}
216.fd-in[data-state="success"][data-focus] .fd-in-box{box-shadow:0 0 0 1.5px var(--n-ok),0 0 0 5px color-mix(in srgb,var(--n-ok) 18%,transparent)}
217.fd-in[data-disabled] .fd-in-box{background:var(--n-dis);box-shadow:0 0 0 1px var(--n-line);opacity:.65}
218.fd-in-main{position:relative;flex:1;min-width:0;display:flex}
219.fd-in-control{appearance:none;-webkit-appearance:none;border:0;outline:0;background:transparent;color:var(--n-fg);width:100%;min-width:0;height:auto;
220 padding:calc(var(--n-h) * .42) 16px calc(var(--n-h) * .14);font-family:inherit;font-size:15px;line-height:1.3;font-weight:400;letter-spacing:inherit;caret-color:var(--n-accent);resize:none;
221 text-overflow:ellipsis}
222.fd-in[data-size="lg"] .fd-in-control{font-size:17px}
223.fd-in[data-multiline] .fd-in-control{padding-top:26px;padding-bottom:12px;resize:vertical;min-height:calc(var(--n-h) * 1.5)}
224.fd-in-control::placeholder{color:var(--n-mut);opacity:.75}
225.fd-in-control:disabled{cursor:not-allowed}
226.fd-in-control:-webkit-autofill{-webkit-text-fill-color:var(--n-fg);-webkit-box-shadow:0 0 0 100px var(--n-bg) inset;transition:background-color 9999s}
227.fd-in-label{position:absolute;left:16px;top:calc(var(--n-h) / 2);max-width:calc(100% - 32px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;color:var(--n-mut);
228 font-size:15px;line-height:1;transform:translateY(-50%);transform-origin:left top;
229 transition:transform var(--fd-dur-2) var(--fd-ease-out),color var(--fd-dur-2) var(--fd-ease-out),top var(--fd-dur-2) var(--fd-ease-out)}
230.fd-in[data-size="lg"] .fd-in-label{font-size:17px}
231.fd-in[data-floated] .fd-in-label{top:calc(var(--n-h) * .3);transform:translateY(-50%) scale(.8)}
232.fd-in[data-multiline]:not([data-floated]) .fd-in-label{top:calc(var(--n-h) / 2)}
233.fd-in[data-focus] .fd-in-label{color:var(--n-accent)}
234.fd-in[data-state="error"] .fd-in-label{color:var(--n-err)}
235.fd-in[data-state="success"] .fd-in-label{color:var(--n-ok)}
236.fd-in-fix{display:flex;align-items:center;color:var(--n-mut);font-size:15px;white-space:nowrap;user-select:none}
237.fd-in-pre{padding:calc(var(--n-h) * .2) 0 0 16px}
238.fd-in-pre+.fd-in-main .fd-in-control{padding-left:8px}.fd-in-pre+.fd-in-main .fd-in-label{left:8px}
239.fd-in-end{display:flex;align-items:center;gap:2px;padding-right:12px}
240.fd-in-suf{padding:calc(var(--n-h) * .2) 4px 0 4px}
241.fd-in-btn{appearance:none;-webkit-appearance:none;border:0;outline:0;cursor:pointer;display:grid;place-items:center;width:32px;height:32px;border-radius:9999px;background:transparent;color:var(--n-mut);
242 transform:scale(1);transition:transform var(--fd-dur-3) var(--fd-spring),background-color var(--fd-dur-1) var(--fd-ease-out),color var(--fd-dur-1) var(--fd-ease-out)}
243@media (hover:hover){.fd-in-btn:hover{background:color-mix(in srgb,var(--n-fg) 7%,transparent);color:var(--n-fg)}}
244.fd-in-btn:active{transform:scale(.92);transition:transform var(--fd-dur-1) var(--fd-ease-out)}
245.fd-in-btn:focus-visible{outline:2px solid color-mix(in srgb,var(--n-accent) 70%,transparent);outline-offset:1px}
246.fd-in-state{display:grid;place-items:center;width:28px;height:28px;margin-left:2px;border-radius:9999px;animation:fd-in-pop var(--fd-dur-3) var(--fd-spring)}
247.fd-in-ok{color:var(--n-ok)}.fd-in-bad{color:var(--n-err)}
248.fd-in-ok path{stroke-dasharray:24;stroke-dashoffset:24;animation:fd-in-draw 380ms var(--fd-ease-out) 80ms forwards}
249.fd-in-foot{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:0 4px}
250.fd-in-msgwrap{display:grid;grid-template-rows:0fr;flex:1;transition:grid-template-rows var(--fd-dur-3) var(--fd-ease-out)}
251.fd-in-msgwrap[data-open]{grid-template-rows:1fr}
252.fd-in-msgin{overflow:hidden;min-height:0}
253.fd-in-msg{padding-top:8px;font-size:13px;line-height:1.35;color:var(--n-mut)}
254.fd-in-err{color:var(--n-err);animation:fd-in-slide var(--fd-dur-3) var(--fd-ease-out)}
255.fd-in-okmsg{color:var(--n-ok)}
256.fd-in-count{padding-top:8px;font-size:12px;line-height:1.35;color:var(--n-mut);font-variant-numeric:tabular-nums;transition:color var(--fd-dur-2) var(--fd-ease-out)}
257.fd-in-count[data-near]{color:var(--n-err)}
258@keyframes fd-in-pop{from{opacity:0;transform:scale(.4)}}
259@keyframes fd-in-draw{to{stroke-dashoffset:0}}
260@keyframes fd-in-slide{from{opacity:0;transform:translateY(-4px)}}
261@media (prefers-reduced-motion:reduce){.fd-in-label,.fd-in-box,.fd-in-msgwrap{transition-duration:1ms}.fd-in-state,.fd-in-err{animation:none}.fd-in-ok path{animation:none;stroke-dashoffset:0}.fd-in-btn,.fd-in-btn:active{transform:none;transition:none}}
262@media (prefers-contrast:more){.fd-in-box{box-shadow:0 0 0 1px currentColor}}
263`;
264
265/* ───────────────────────────── demo ───────────────────────────── */
266
267const emailOk = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v);
268
269function pwScore(v: string) {
270 let s = 0;
271 if (v.length >= 10) s++;
272 if (/[A-Z]/.test(v) && /[a-z]/.test(v)) s++;
273 if (/\d/.test(v)) s++;
274 if (/[^A-Za-z0-9]/.test(v)) s++;
275 return s;
276}
277
278export function Demo() {
279 const [theme, setTheme] = useState<"light" | "dark">("light");
280 const [name, setName] = useState("Amara Okonkwo");
281 const [email, setEmail] = useState("amara.okonkwo@");
282 const [emailTouched, setEmailTouched] = useState(true);
283 const [phone, setPhone] = useState("(415) 555-0142");
284 const [member, setMember] = useState("BCX-2204-9187");
285 const [pharmacy, setPharmacy] = useState("Mission Bay Pharmacy");
286 const [note, setNote] = useState("Prefers text reminders before 6 pm. Allergic to penicillin.");
287 const [pw, setPw] = useState("");
288 const [pwTouched, setPwTouched] = useState(false);
289
290 const score = pwScore(pw);
291 const emailErr = emailTouched && !emailOk(email) ? (email ? "Enter a full address, like amara@example.com" : "Email is required so we can send visit summaries") : undefined;
292
293 return (
294 <div className="fd-in-demo" data-fd-theme={theme}>
295 <style>{demoCss}</style>
296 <header className="fd-in-demo-bar">
297 <span className="fd-in-demo-brand"><i aria-hidden="true" />Northfield Health</span>
298 <span className="fd-in-demo-who">Amara O.</span>
299 <button type="button" className="fd-in-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>
300 </header>
301
302 <main className="fd-in-demo-main">
303 <div className="fd-in-demo-title">
304 <h1>Account settings</h1>
305 <p>Keep your contact details current so your care team can reach you.</p>
306 </div>
307
308 <section className="fd-in-demo-card" aria-labelledby="fd-in-profile">
309 <div className="fd-in-demo-cardhead"><h2 id="fd-in-profile">Personal information</h2><p>Shown to your care team and on visit summaries.</p></div>
310 <div className="fd-in-demo-grid">
311 <InputField label="Full name" value={name} onChange={setName} clearable autoComplete="name" success={name.trim().split(" ").length > 1 ? "Matches your insurance card" : false} />
312 <InputField label="Email" type="email" value={email} onChange={setEmail} onBlur={() => setEmailTouched(true)} error={emailErr} success={!emailErr && emailOk(email)} autoComplete="email" helper="Used for visit summaries and billing." />
313 <InputField label="Mobile phone" type="tel" prefix="+1" value={phone} onChange={setPhone} helper="We text appointment reminders. Msg rates may apply." autoComplete="tel-national" />
314 <InputField label="Insurance member ID" value={member} onChange={setMember} suffix="Blue Cross" helper="Find it on the front of your card." />
315 <InputField label="Preferred pharmacy" value={pharmacy} onChange={setPharmacy} clearable placeholder="Search by name or ZIP" />
316 <InputField label="Date of birth" value="1988-03-14" readOnly helper="Contact support to change a verified date of birth." />
317 </div>
318 <InputField label="Notes for your care team" multiline rows={3} maxLength={120} value={note} onChange={setNote} helper="Visible to clinicians only." />
319 </section>
320
321 <section className="fd-in-demo-card" aria-labelledby="fd-in-sec">
322 <div className="fd-in-demo-cardhead"><h2 id="fd-in-sec">Security</h2><p>Use at least 10 characters with a mix of letters, numbers and symbols.</p></div>
323 <div className="fd-in-demo-grid">
324 <div>
325 <InputField
326 label="New password"
327 type="password"
328 value={pw}
329 onChange={setPw}
330 onBlur={() => setPwTouched(true)}
331 autoComplete="new-password"
332 error={pwTouched && pw && score < 3 ? "Too easy to guess. Add a number or symbol." : undefined}
333 success={score >= 3 ? "Strong password" : false}
334 />
335 <div className="fd-in-demo-meter" aria-hidden="true" data-score={score}><i /><i /><i /><i /></div>
336 </div>
337 <InputField label="Current password" type="password" disabled defaultValue="••••••••••" helper="Sign in again to confirm changes." />
338 </div>
339 </section>
340
341 <div className="fd-in-demo-actions">
342 <button type="button" className="fd-in-demo-btn" data-v="ghost">Cancel</button>
343 <button type="button" className="fd-in-demo-btn" data-v="primary">Save changes</button>
344 </div>
345 </main>
346 </div>
347 );
348}
349
350const demoCss = `
351:where(.fd-in-demo,.fd-in-demo *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
352:where(.fd-in-demo,.fd-in-demo *)::before,:where(.fd-in-demo,.fd-in-demo *)::after{box-sizing:border-box}
353:where(.fd-in-demo button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
354.fd-in-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;
355 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-dur-2:240ms;
356 --d-bg:#F5F6F8;--d-card:#fff;--d-ink:#0B0B0F;--d-mut:#6B6B76;--d-line:rgba(11,11,15,.08);--d-accent:#007AFF;--d-teal:#0E8F7E;
357 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)}
358.fd-in-demo[data-fd-theme="dark"]{--d-bg:#09090B;--d-card:#111115;--d-ink:#F5F5F7;--d-mut:#9A9AA6;--d-line:rgba(255,255,255,.09);--d-accent:#0A84FF;--d-teal:#2DD4BF}
359.fd-in-demo-bar{display:flex;align-items:center;gap:16px;height:60px;padding:0 clamp(16px,4vw,40px);background:var(--d-card);border-bottom:1px solid var(--d-line)}
360.fd-in-demo-brand{display:flex;align-items:center;gap:10px;margin-right:auto;font:600 16px/1 var(--d-disp);letter-spacing:-.02em}
361.fd-in-demo-brand i{display:block;width:20px;height:20px;border-radius:6px;background:var(--d-teal);position:relative}
362.fd-in-demo-brand i::before,.fd-in-demo-brand i::after{content:"";position:absolute;background:#fff;border-radius:1px}
363.fd-in-demo-brand i::before{left:9px;top:4px;width:2px;height:12px}.fd-in-demo-brand i::after{left:4px;top:9px;width:12px;height:2px}
364.fd-in-demo-who{font:400 14px/1 var(--d-font);color:var(--d-mut)}
365.fd-in-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)}
366.fd-in-demo-theme:hover{color:var(--d-ink)}.fd-in-demo-theme:focus-visible,.fd-in-demo-btn:focus-visible{outline:2px solid rgba(10,132,255,.7);outline-offset:2px}
367.fd-in-demo-main{max-width:760px;margin:0 auto;padding:40px clamp(16px,4vw,40px) 72px;display:flex;flex-direction:column;gap:20px}
368.fd-in-demo-title h1{font:600 28px/1.15 var(--d-disp);letter-spacing:-.025em}
369.fd-in-demo-title p{margin-top:8px;font:400 15px/1.4 var(--d-font);color:var(--d-mut)}
370.fd-in-demo-card{display:flex;flex-direction:column;gap:20px;padding:28px;border-radius:24px;background:var(--d-card);box-shadow:0 0 0 1px var(--d-line),0 1px 2px rgba(0,0,0,.03),0 16px 32px -24px rgba(0,0,0,.18)}
371.fd-in-demo-cardhead h2{font:600 17px/1.2 var(--d-font);letter-spacing:-.01em}
372.fd-in-demo-cardhead p{margin-top:6px;font:400 14px/1.4 var(--d-font);color:var(--d-mut)}
373.fd-in-demo-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 16px;align-items:start}
374.fd-in-demo-meter{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin:10px 4px 0}
375.fd-in-demo-meter i{height:4px;border-radius:9999px;background:var(--d-line);transition:background-color var(--fd-dur-2) var(--fd-ease-out)}
376.fd-in-demo-meter[data-score="1"] i:nth-child(-n+1){background:#FF453A}.fd-in-demo-meter[data-score="2"] i:nth-child(-n+2){background:#FF9F0A}
377.fd-in-demo-meter[data-score="3"] i:nth-child(-n+3){background:#30D158}.fd-in-demo-meter[data-score="4"] i{background:#30D158}
378.fd-in-demo-actions{display:flex;justify-content:flex-end;gap:8px}
379.fd-in-demo-btn{cursor:pointer;height:44px;padding:0 22px;border-radius:9999px;font:500 15px/1 var(--d-font);transition:transform 420ms cubic-bezier(.34,1.56,.64,1)}
380.fd-in-demo-btn:active{transform:scale(.96);transition-duration:120ms}
381.fd-in-demo-btn[data-v="primary"]{background:var(--d-accent);color:#fff;box-shadow:0 1px 0 rgba(255,255,255,.22) inset,0 1px 2px rgba(0,40,120,.3)}
382.fd-in-demo-btn[data-v="ghost"]{color:var(--d-mut)}.fd-in-demo-btn[data-v="ghost"]:hover{color:var(--d-ink)}
383@media (max-width:640px){.fd-in-demo-grid{grid-template-columns:1fr}.fd-in-demo-card{padding:20px;border-radius:20px}}
384`;

More Forms. Same file contract.

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.

Multi-step Signup

A three-step onboarding flow with progress, validation, animated transitions and a review step.