OTP Input
Six-digit code entry with auto-advance, paste and SMS autofill, error shake and a success cascade.
A floating-label input with helper, error and success states, adornments, counter, clear and password reveal.
npx shadcn@latest add https://freedesignmd.com/r/input-field.json1/**2 * InputField — text input with a floating label, helper / error / success3 * states, prefix + suffix adornments, character counter, clear button and a4 * 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 progressive13 * 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 toggle16 * is a <button aria-pressed> with a label, the clear button is keyboard17 * 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 ...rest67 },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 engines84 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 <InputField326 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`;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.
A three-step onboarding flow with progress, validation, animated transitions and a review step.