Introduce NumberField, CurrencyInput, PasswordInput, and improve the existing OtpInput. These specialized form inputs address gaps in numeric entry, monetary formatting, secure password entry, and OTP usability. All components integrate with React Hook Form and Zod, use React Aria where appropriate, and ship with comprehensive Storybook stories.
Introduction
Overview
The design system provides TextField, TextArea, Select, CurrencyField, and OtpInput as form primitives. However, teams repeatedly build ad-hoc number inputs with increment/decrement buttons, password fields with visibility toggles, and currency inputs needing locale-aware formatting beyond what CurrencyField offers. The OtpInput also lacks paste-from-clipboard reliability and error-shake animation.
Goals
Provide a NumberField with increment/decrement stepper buttons, min/max/step validation, and keyboard support.
Provide a CurrencyInput with locale-aware formatting (thousands separator, decimal precision), building on CurrencyField.
Provide a PasswordInput with visibility toggle, strength meter, and configurable requirements display.
Improve OtpInput with reliable paste handling, auto-submit on completion, error shake animation, and expiry countdown.
Deliver Zod validation schema examples for each input in Storybook docs.
Non-Goals
Credit card number formatting (separate component; different masking rules).
Phone number input with country code picker (separate internationalization concern).
Replacing TextField as the general-purpose text input.
Scope
In Scope
Item
Description
NumberField
Numeric input with stepper buttons, min/max/step, keyboard arrows to increment/decrement.
CurrencyInput
Locale-aware currency formatting with thousands separators, decimal precision, and symbol placement.
PasswordInput
Password field with visibility toggle, optional strength meter, and requirements checklist.
All components work as controlled and uncontrolled inputs within RHF.
Zod validation examples
Storybook docs include Zod schema recipes for each input type.
Stories and tests
Complete Storybook coverage with controls; unit tests for keyboard interactions and validation.
Out of Scope
Item
Reason
Credit card input
Different masking and Luhn validation; deserves its own FRD.
Phone number input
Country code data and libphonenumber dependency are heavy; separate effort.
Slider/range input
Different interaction model; not a text-field derivative.
Users and Pain Points
User
Pain Point
Form developers
No native number stepper; teams use <input type="number"> which has inconsistent browser UX and no design-system styling.
E-commerce/billing teams
CurrencyField does not format on-the-fly (thousands separators while typing); manual formatting code is error-prone.
Auth/security teams
Password inputs lack integrated strength meters; visibility toggles are hand-built per project.
Login/2FA flows
OtpInput paste from SMS auto-fill is unreliable; no visual feedback on incorrect codes.
Definitions
Term
Definition
Stepper buttons
Increment (+) and decrement (−) buttons flanking a numeric input.
Strength meter
A visual bar indicating password strength (weak/fair/strong/very strong) based on configurable rules.
Locale-aware formatting
Formatting numbers according to the user’s locale (e.g., 1,234.56 for en-US, 1.234,56 for de-DE).
Auto-submit
Automatically firing a callback when all OTP cells are filled without requiring a submit button click.
Error shake
A brief horizontal oscillation animation applied to the input on validation failure.
Current State
TextField (src/components/ui/text-field.tsx): General-purpose text input with label, error, hint, leading/trailing icons, currency mode. Foundation for all text-based inputs.
CurrencyField (src/components/ui/currency-field.tsx): Thin wrapper over TextField with currency mode, inputMode="decimal", and leading currency symbol. Does not format thousands separators while typing.
OtpInput (src/components/ui/otp-input.tsx): Individual digit cells with focus management. Supports length, value, onChange, onComplete. Paste handling works for simple cases but fails with SMS auto-fill on some mobile browsers. No error animation.
No NumberField or PasswordInput exists.
Proposed Solution
NumberField
Build src/components/ui/number-field.tsx using React Aria’s useNumberField hook for accessibility and locale-aware number parsing. The component renders an input flanked by decrement (−) and increment (+) buttons. Arrow Up/Down keyboard shortcuts increment/decrement by step. Holding Shift multiplies step by 10.
CurrencyInput
Build src/components/ui/currency-input.tsx extending CurrencyField with live formatting. As the user types, the value is formatted with thousands separators using Intl.NumberFormat. On blur, the value is normalized to the specified decimal precision. A locale prop controls formatting rules. The component emits a numeric value (not a formatted string) via onChange.
PasswordInput
Build src/components/ui/password-input.tsx wrapping TextField with a trailing visibility toggle (eye/eye-off icon) using SecretVisibilityToggle patterns. An optional strengthMeter prop enables a color-coded bar below the input. An optional requirements prop accepts an array of { label: string; test: RegExp | ((value: string) => boolean) } to show a live checklist.
OtpInput Improvements
Enhance the existing src/components/ui/otp-input.tsx:
Fix paste handling to work with SMS auto-fill by listening to the input event with inputType === "insertFromPaste".
Add autoSubmit prop (default false) that triggers onComplete and optionally calls a provided onAutoSubmit.
Add error shake animation using motion.css tokens and a shake prop or automatic trigger on error prop change.
Add optional expiresIn prop (seconds) rendering a countdown timer with resend action.
Requirements
Requirement Priorities
Must Have: NumberField, PasswordInput with visibility toggle, OtpInput paste fix.
Should Have: CurrencyInput live formatting, PasswordInput strength meter, OtpInput error shake.
Could Have: PasswordInput requirements checklist, OtpInput expiry countdown, NumberField large-step with Shift.
Track cursor position before/after formatting; restore logical position.
NumberField stepper conflicts with browser native spinner
Low
Low
Apply appearance: textfield to hide native spinner; our steppers take over.
OtpInput SMS auto-fill varies wildly across mobile browsers
Medium
Medium
Test on BrowserStack across top 10 mobile browser versions; add fallback paste listener.
PasswordInput strength algorithm may not match server-side rules
Low
Medium
Strength meter is advisory only; document that server validation is authoritative.
Open Questions
#
Question
Owner
Status
OQ-01
Should NumberField support formatted display (thousands separators) or remain raw numeric?
David Holmes
Open
OQ-02
Should CurrencyInput replace CurrencyField or exist alongside it?
David Holmes
Open
OQ-03
What password strength algorithm to use — zxcvbn or a simpler regex-based scorer?
David Holmes
Open
OQ-04
Should OtpInput support alphanumeric codes or remain digits-only?
David Holmes
Open
Acceptance Criteria
#
Criterion
AC-01
NumberField renders stepper buttons, respects min/max/step, and supports keyboard increment/decrement.
AC-02
CurrencyInput formats with locale-appropriate thousands separators while typing without cursor jumps.
AC-03
PasswordInput toggles visibility, displays strength meter, and shows requirements checklist.
AC-04
OtpInput reliably handles paste on desktop and mobile; error state triggers shake animation.
AC-05
All components work within React Hook Form with Controller pattern.
AC-06
All components pass axe-core checks with zero violations.
AC-07
Storybook stories exist with controls for all props.
AC-08
pnpm typecheck and pnpm vitest run --project unit pass with zero errors.
LLM Handoff Instructions
When implementing this FRD:
Start with NumberField — src/components/ui/number-field.tsx. Use React Aria’s useNumberField for the spinbutton pattern. Follow TextField’s prop conventions. Stepper buttons should use the existing Button component with size="sm" and variant="ghost".
Then PasswordInput — src/components/ui/password-input.tsx. Wrap TextField with type="password" toggled to type="text". Reuse the icon toggle pattern from secret-visibility-toggle.tsx. The strength meter is a separate sub-component rendered below the input.
Then CurrencyInput — src/components/ui/currency-input.tsx. Extend CurrencyField with Intl.NumberFormat for live formatting. Store the raw numeric value internally; format for display only. Handle cursor position restoration carefully.
Then OtpInput improvements — modify src/components/ui/otp-input.tsx in place. Add paste event handling, shake animation (use motion.css duration tokens), and optional expiry countdown.
Stories follow the existing pattern in sibling .stories.tsx files.
Tests in sibling .test.tsx files. Use userEvent for keyboard interactions, clipboard paste simulation.
Key files to reference:
src/components/ui/text-field.tsx — base input pattern and props.