Skip to content

FRD-027: Specialized Form Inputs

FieldValue
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
Target Releasev2.0.0
TypeWidget
SizeL
PriorityP1 — Component Improvement

Document Summary

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

ItemDescription
NumberFieldNumeric input with stepper buttons, min/max/step, keyboard arrows to increment/decrement.
CurrencyInputLocale-aware currency formatting with thousands separators, decimal precision, and symbol placement.
PasswordInputPassword field with visibility toggle, optional strength meter, and requirements checklist.
OtpInput improvementsPaste handling fix, auto-submit callback, error shake animation, optional expiry countdown.
React Hook Form integrationAll components work as controlled and uncontrolled inputs within RHF.
Zod validation examplesStorybook docs include Zod schema recipes for each input type.
Stories and testsComplete Storybook coverage with controls; unit tests for keyboard interactions and validation.

Out of Scope

ItemReason
Credit card inputDifferent masking and Luhn validation; deserves its own FRD.
Phone number inputCountry code data and libphonenumber dependency are heavy; separate effort.
Slider/range inputDifferent interaction model; not a text-field derivative.

Users and Pain Points

UserPain Point
Form developersNo native number stepper; teams use <input type="number"> which has inconsistent browser UX and no design-system styling.
E-commerce/billing teamsCurrencyField does not format on-the-fly (thousands separators while typing); manual formatting code is error-prone.
Auth/security teamsPassword inputs lack integrated strength meters; visibility toggles are hand-built per project.
Login/2FA flowsOtpInput paste from SMS auto-fill is unreliable; no visual feedback on incorrect codes.

Definitions

TermDefinition
Stepper buttonsIncrement (+) and decrement (−) buttons flanking a numeric input.
Strength meterA visual bar indicating password strength (weak/fair/strong/very strong) based on configurable rules.
Locale-aware formattingFormatting numbers according to the user’s locale (e.g., 1,234.56 for en-US, 1.234,56 for de-DE).
Auto-submitAutomatically firing a callback when all OTP cells are filled without requiring a submit button click.
Error shakeA 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.
  • Won’t Have (this release): Masked input (arbitrary patterns), credit card input.

Functional Requirements

IDRequirementPriority
FR-01NumberField accepts value (number), onChange, min, max, step (default 1), label, error, hint, disabled, required.Must
FR-02NumberField renders increment/decrement stepper buttons; buttons disable at min/max boundaries.Must
FR-03NumberField supports Arrow Up/Down for increment/decrement; Shift+Arrow multiplies step by 10.Should
FR-04NumberField clamps value to min/max on blur.Must
FR-05CurrencyInput accepts value (number), onChange, locale (BCP 47 string), currency (ISO 4217 code), precision (decimal places).Should
FR-06CurrencyInput formats with thousands separators while typing and normalizes on blur.Should
FR-07PasswordInput extends TextField with a trailing visibility toggle (show/hide password).Must
FR-08PasswordInput strengthMeter prop enables a 4-segment strength bar (weak/fair/strong/very strong).Should
FR-09PasswordInput requirements prop renders a checklist of password rules with live pass/fail indicators.Could
FR-10OtpInput reliably handles paste from clipboard and SMS auto-fill on iOS and Android.Must
FR-11OtpInput error prop change triggers a shake animation using motion tokens.Should
FR-12OtpInput expiresIn prop renders a countdown timer with a “Resend” action button.Could
FR-13All components integrate with React Hook Form via register or Controller without wrapper hacks.Must

Non-Functional Requirements

IDRequirementTarget
NFR-01NumberField stepper click response time< 50ms perceived latency per click.
NFR-02CurrencyInput formatting delay< 16ms per keystroke for live formatting.
NFR-03PasswordInput strength calculation< 5ms per keystroke.
NFR-04Bundle size per component< 5 KB gzipped (NumberField, PasswordInput); < 3 KB (CurrencyInput).
NFR-05OtpInput paste success rate100% on Chrome, Safari, Firefox latest; 95%+ on iOS Safari and Chrome Android.

API/Interface Requirements

NumberField

interface NumberFieldProps {
value?: number;
onChange?: (value: number) => void;
min?: number;
max?: number;
step?: number; // default 1
label?: string;
error?: string;
hint?: string;
disabled?: boolean;
required?: boolean;
className?: string;
}

CurrencyInput

interface CurrencyInputProps {
value?: number;
onChange?: (value: number) => void;
locale?: string; // BCP 47, default "en-US"
currency?: string; // ISO 4217, default "USD"
precision?: number; // decimal places, default 2
label?: string;
error?: string;
hint?: string;
disabled?: boolean;
required?: boolean;
className?: string;
}

PasswordInput

interface PasswordRequirement {
label: string;
test: RegExp | ((value: string) => boolean);
}
interface PasswordInputProps {
value?: string;
onChange?: (value: string) => void;
strengthMeter?: boolean;
requirements?: PasswordRequirement[];
label?: string;
error?: string;
hint?: string;
disabled?: boolean;
required?: boolean;
className?: string;
}

OtpInput Additions

// New props added to existing OtpInputProps
interface OtpInputProps {
// ... existing props ...
autoSubmit?: boolean;
onAutoSubmit?: (value: string) => void;
shake?: boolean;
expiresIn?: number; // seconds
onResend?: () => void;
resendLabel?: string;
}

Accessibility Requirements

IDRequirement
A11Y-01NumberField uses role="spinbutton" with aria-valuemin, aria-valuemax, aria-valuenow.
A11Y-02Stepper buttons have aria-label (“Increase value” / “Decrease value”) and announce value changes.
A11Y-03PasswordInput visibility toggle has aria-label (“Show password” / “Hide password”) and aria-pressed.
A11Y-04Strength meter uses role="meter" with aria-valuemin=0, aria-valuemax=4, aria-valuenow, aria-label="Password strength".
A11Y-05OtpInput error shake does not rely on motion alone; error text is announced via role="alert".
A11Y-06All components pass axe-core automated checks with zero violations.

Content and Documentation Requirements

IDRequirement
DOC-01Each component gets a Storybook docs page with usage guidelines, prop table, and interactive examples.
DOC-02Zod validation recipes for: number ranges, currency amounts, password strength, OTP format.
DOC-03React Hook Form integration example for each component.
DOC-04Migration guide from raw &lt;input type="number"&gt; and &lt;input type="password"&gt; to new components.

Dependencies

DependencyTypeRisk
react-aria-components / react-ariaExistingLow — useNumberField hook provides spinbutton accessibility.
src/components/ui/text-field.tsxInternalLow — PasswordInput and NumberField extend its patterns.
src/components/ui/currency-field.tsxInternalLow — CurrencyInput builds on it.
src/components/ui/otp-input.tsxInternalLow — Enhancement, not replacement.
src/components/ui/secret-visibility-toggle.tsxInternalLow — PasswordInput reuses visibility toggle pattern.
Intl.NumberFormatBrowser APILow — Supported in all target browsers.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
CurrencyInput live formatting causes cursor jumpMediumHighTrack cursor position before/after formatting; restore logical position.
NumberField stepper conflicts with browser native spinnerLowLowApply appearance: textfield to hide native spinner; our steppers take over.
OtpInput SMS auto-fill varies wildly across mobile browsersMediumMediumTest on BrowserStack across top 10 mobile browser versions; add fallback paste listener.
PasswordInput strength algorithm may not match server-side rulesLowMediumStrength meter is advisory only; document that server validation is authoritative.

Open Questions

#QuestionOwnerStatus
OQ-01Should NumberField support formatted display (thousands separators) or remain raw numeric?David HolmesOpen
OQ-02Should CurrencyInput replace CurrencyField or exist alongside it?David HolmesOpen
OQ-03What password strength algorithm to use — zxcvbn or a simpler regex-based scorer?David HolmesOpen
OQ-04Should OtpInput support alphanumeric codes or remain digits-only?David HolmesOpen

Acceptance Criteria

#Criterion
AC-01NumberField renders stepper buttons, respects min/max/step, and supports keyboard increment/decrement.
AC-02CurrencyInput formats with locale-appropriate thousands separators while typing without cursor jumps.
AC-03PasswordInput toggles visibility, displays strength meter, and shows requirements checklist.
AC-04OtpInput reliably handles paste on desktop and mobile; error state triggers shake animation.
AC-05All components work within React Hook Form with Controller pattern.
AC-06All components pass axe-core checks with zero violations.
AC-07Storybook stories exist with controls for all props.
AC-08pnpm typecheck and pnpm vitest run --project unit pass with zero errors.

LLM Handoff Instructions

When implementing this FRD:

  1. Start with NumberFieldsrc/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".
  2. Then PasswordInputsrc/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.
  3. Then CurrencyInputsrc/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.
  4. 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.
  5. Stories follow the existing pattern in sibling .stories.tsx files.
  6. 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.
  • src/components/ui/currency-field.tsx — existing currency wrapper.
  • src/components/ui/otp-input.tsx — current implementation to enhance.
  • src/components/ui/secret-visibility-toggle.tsx — visibility toggle pattern.
  • src/lib/form-control-styles.ts — field shell styling.

Decision Log

DateDecisionRationale
2026-05-26NumberField emits raw number values, not formatted strings.Consumers should not parse display strings; formatting is a presentation concern.
2026-05-26CurrencyInput exists alongside CurrencyField rather than replacing it.CurrencyField is simpler and sufficient for many use cases; CurrencyInput adds live formatting for advanced needs.
2026-05-26PasswordInput strength meter is a visual hint, not a validation gate.Server-side validation is authoritative; client-side strength is advisory.
2026-05-26OtpInput improvements are in-place enhancements, not a new component.Avoids migration burden for existing consumers; new props are all optional.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.