rn-inputforge
v0.5.2
Published
A production-ready React Native input/form system (bare CLI & Expo) — address/money/consent fields, toasts, bottom sheets, split rows, morph/haptics, density theming, autofill, wizard, drafts, and a full field kit. Pure JS/TS, no native modules.
Maintainers
Keywords
Readme
rn-inputforge
A production-ready input & form system for React Native (bare CLI and Expo). Pure JS/TS — no native modules, works with npm or yarn.
One package for fields, validation, theming, motion, drafts, and form UX — so you don’t bolt together five libraries for a signup screen.
What's included
Everything ships from rn-inputforge. Use this map to find the right export.
Fields & controls
| You need… | Use |
| --------- | --- |
| Text / email / phone / password / search / number | FieldFlowInput |
| Phone + country dial code | FieldFlowPhone |
| OTP / PIN digits | FieldFlowOTP |
| Card / phone / currency / date mask | FieldFlowMaskedInput |
| Card number + expiry + CVC | FieldFlowCreditCard |
| Date / time / datetime | FieldFlowDateTimePicker |
| Checkbox / radio / switch | Checkbox, radio & switch |
| Multi-value chips (skills, tags) | FieldFlowTags |
| Single or multi select | FieldFlowDropdown |
| Async search / typeahead | FieldFlowTypeahead |
| File / image upload (pluggable picker) | FieldFlowUpload |
| Star rating | FieldFlowRating |
| Range slider | FieldFlowSlider |
| +/- quantity stepper | FieldFlowNumberStepper |
| Signature pad | FieldFlowSignature |
| Plan / filter pill tabs | FieldFlowSegmented |
| Structured address | FieldFlowAddress |
| Money / percent | FieldFlowMoney · FieldFlowPercent |
| Color swatches / avatar | FieldFlowColor · FieldFlowAvatarPicker |
| Consent / scroll-to-accept terms | FieldFlowConsent · FieldFlowTermsGate |
| Confirm-details review | FieldFlowReviewSummary |
| Long bio / notes | DescriptionField |
| Chat composer + attachments | ChatInput |
Form engine
| You need… | Use |
| --------- | --- |
| Full form state + submit | useFieldFlowForm |
| Sync / async / cross-field rules | Validation · Zod |
| Server / API field errors (422) | setErrors / setFieldError |
| Localized validation messages | locale / messages |
| Multi-step wizard | useFieldFlowSteps + FieldFlowStepper |
| Warn before leaving dirty form | useFieldFlowDirtyGuard |
| Show / hide / require fields by values | Conditional fields |
| Repeatable rows (guests, line items) | useFieldFlowArray |
| Resume unfinished forms | useFieldFlowDraft + FieldFlowDraftBanner |
| Keyboard next / done across fields | useKeyboardManager + KeyboardAwareFormWrapper |
| Friction analytics + a11y on failed submit | Form intelligence |
| Autofill / password manager hints | autofill / autofillProfiles |
| Keyboard Prev/Next/Done bar | FieldFlowKeyboardToolbar |
| Smart paste (OTP / card / address) | routeSmartPaste |
| “N fields left” progress coach | FieldFlowCompleteness |
| Passkey / SSO / biometric strip | FieldFlowAuthActions |
| JSON schema → form UI | FieldFlowSchemaForm |
| Dev accessibility audit | FieldFlowA11yAudit |
| Offline submit retry queue | useFieldFlowSubmitQueue |
| Already on React Hook Form | FieldFlowController |
UI polish & branding
| You need… | Use |
| --------- | --- |
| Group fields in a card / collapse | FieldFlowSection |
| Focus glow, error shake, success pop | motion on inputs · useFieldMotion |
| +92 / $ / kg inside the field | prefix / suffix |
| Email domain / dial-code chips | suggestions · FieldFlowAffordance |
| Password meter | showStrength |
| Look: outlined / filled / underline / floating | Variants |
| Built-in icons (no icon pack) | Icons |
| Brand colors in one line | themePresets · FieldFlowThemeProvider |
| Density / size / shape / surface | density size shape surface focusRing |
| Calm tips & alerts | FieldFlowInlineAlert · FieldFlowInfoTip |
| Loading skeletons | FieldFlowSkeleton |
| Form intro header | FieldFlowFormHeader |
| Empty / no-results | FieldFlowEmptyState |
| Toasts & banner stack | FieldFlowToastProvider · FieldFlowBannerStack |
| Dividers / or-gate | FieldFlowDivider · FieldFlowOrGate |
| Shared label / side-by-side | FieldFlowFieldGroup · FieldFlowSplitRow |
| Bottom sheet field | FieldFlowBottomSheetField |
| Error/success morph + float presets | morph · floatPreset |
| Field haptics | haptics · useFieldFlowHaptics |
| Per-field theme crossfade | themeCrossfade |
Helpers (also exported)
Masks (applyMask, unmask, …) · password strength (evaluatePasswordStrength) · affordance helpers · a11y (focusFirstError, announceForAccessibility) · validators · Zod adapters · FieldFlowIcon / renderFieldFlowIcon
Full walkthrough: Quick start · Try it · Public API
What's new in 0.5.2
| Addition | What you get |
| -------- | ------------ |
| EmptyState | Start-typing / empty / no-results surfaces |
| Toast + BannerStack | Transient toasts + stacked form banners |
| Divider / OrGate | “or continue with email” chrome |
| FieldGroup / SplitRow | Shared labels + side-by-side layouts |
| BottomSheetField | Reusable sheet for long pickers |
| Morph + float presets | Error pulse, success morph, subtle / dramatic / material |
| Haptics kit | Optional Vibration (or inject expo-haptics) |
| Per-field theme crossfade | Soft opacity when a field theme changes |
| Address / Money / Percent | Structured address + currency/% fields |
| Color / AvatarPicker | Swatches + profile avatar chrome |
| Consent / TermsGate | Linked legal checkbox + scroll-to-accept |
| ReviewSummary | Read-only confirm step before submit |
What's new in 0.5.0
| Addition | What you get |
| -------- | ------------ |
| Smart Affordance | Email / phone / currency suggestion chips |
| Form Intelligence | Friction report + autofocus / announce on failed submit |
| Conditional fields | showWhen / requiredWhen / disabledWhen |
| Field arrays | useFieldFlowArray |
| Draft resume | useFieldFlowDraft + FieldFlowDraftBanner |
| Password strength | showStrength / FieldFlowPasswordStrength |
| Tags | FieldFlowTags |
| React Hook Form | FieldFlowController / FieldFlowTagsController |
| Motion UI | Glow, shake, fade, success pop (motion prop) |
| Sections | FieldFlowSection |
| Theme presets | themePresets.soft / warm / teal / midnight / contrast |
| Prefix / suffix | Inline affixes on inputs |
| Segmented | FieldFlowSegmented |
| Multi-step wizard | useFieldFlowSteps + FieldFlowStepper |
| Phone + country | FieldFlowPhone |
| File upload | FieldFlowUpload (inject your picker) |
| Typeahead | FieldFlowTypeahead (async fetchOptions) |
| Server errors | setErrors / setFieldError / clearErrors |
| Rating / slider / +/- | FieldFlowRating, FieldFlowSlider, FieldFlowNumberStepper |
| Dirty leave guard | useFieldFlowDirtyGuard + isDirty |
| i18n messages | locale / messages (en es fr de pt ar) |
| Credit card group | FieldFlowCreditCard |
| Signature pad | FieldFlowSignature |
| Autofill profiles | autofill prop + autofillProfiles.login/signup/… |
| Keyboard toolbar | FieldFlowKeyboardToolbar Prev/Next/Done |
| Smart paste | routeSmartPaste / useSmartPaste |
| Completeness coach | useFieldFlowCompleteness + FieldFlowCompleteness |
| Auth actions | FieldFlowAuthActions (passkey / SSO / biometric slots) |
| Schema form | FieldFlowSchemaForm JSON → UI |
| A11y auditor | runA11yAudit + FieldFlowA11yAudit (dev) |
| Submit queue | useFieldFlowSubmitQueue offline / retry |
| Visual system | Density, size, shape, surface, focus rings |
| Inline alert / skeleton / header | FieldFlowInlineAlert, FieldFlowSkeleton, FieldFlowFormHeader |
| Info tips + helper status | infoTip, helperStatus, FieldFlowInfoTip |
| Theme crossfade | Soft fade when switching presets (animateTheme) |
Still from 0.4.0: OTP, masks, date/time, checkbox/radio/switch, variants, icons, async + cross-field validation, Zod, Expo.
Note: since 0.4.0,
validateField/validateAllreturnPromise<boolean>.
Installation
Pick whichever your project already uses — the package itself doesn't care.
# npm
npm install rn-inputforge
# yarn
yarn add rn-inputforgeThat's it — this is a pure JavaScript/TypeScript package (no native code), so there is no pod install step and no manual linking required on bare React Native CLI. Same install works in Expo (Managed or Dev Client) — just npx expo install is not required for this package because it has no native dependencies; npm install rn-inputforge / yarn add rn-inputforge is enough.
Requirements
react>= 18 (tested against 19.2.x)react-native>= 0.73 (tested against 0.86.x)- Works with Expo SDK that ships RN ≥ 0.73
- TypeScript is optional but recommended (type defs are bundled)
zodis an optional peer dependency (only if you use the Zod helpers)react-hook-formis an optional peer dependency (only if you useFieldFlowController)
Quick start
import { FieldFlowInput } from 'rn-inputforge';
function LoginForm() {
const [email, setEmail] = useState('');
return (
<FieldFlowInput
type="email"
label="Email address"
placeholder="[email protected]"
value={email}
onChangeText={setEmail}
required
/>
);
}Setting type="email" automatically wires up the email keyboard, disables autocorrect/autocapitalize, and applies email-format validation on blur — no extra config needed. Same idea for number, decimal, phone, password, and search.
Field types
| type | Keyboard | Notes |
| ------------- | -------------------- | ------------------------------------- |
| text | default | sentence auto-capitalize |
| email | email-address | auto validates email format |
| number | numeric / number-pad | strips non-digits as you type |
| decimal | decimal-pad | allows a single . |
| phone | phone-pad | strips invalid phone characters |
| password | default, masked | built-in show/hide toggle |
| search | default | search icon + clear button by default |
| multiline | default, multi-line | free-form multi-line text |
| description | default, multi-line | used internally by DescriptionField |
| chat | default | used internally by ChatInput |
Core FieldFlowInput props (cheat sheet)
| Prop | Purpose |
| ---- | ------- |
| type | Keyboard + defaults (email, password, phone, …) |
| variant | outlined | filled | underline | floating |
| label / helperText / errorText / successText | Copy + states |
| rules / validateOnChange | Built-in validation |
| leftIcon / rightIcon / iconSize / clearable | Icons (built-in name or custom node) |
| prefix / suffix | Inline affix text or node |
| suggestions | Smart chips (true, 'email', custom list) |
| showStrength | Password strength meter |
| motion | Micro-interactions (default true) |
| maxLength / showCharacterCount | Limits + counter |
| theme / style props | Per-field override |
All standard TextInput props still pass through (placeholder, returnKeyType, etc.).
Validation
Rules are declarative and reusable across the whole library:
<FieldFlowInput
type="email"
label="Email"
value={email}
onChangeText={setEmail}
rules={{ required: 'Email is required', email: true }}
validateOnChange
/>Available rule keys: required, minLength, maxLength, pattern (RegExp), email, number, and custom (one or more (value: string) => { valid: boolean; message?: string } functions).
For a whole form at once, use useFieldFlowForm:
import { useFieldFlowForm, FieldFlowInput } from 'rn-inputforge';
const form = useFieldFlowForm({
initialValues: { name: '', email: '' },
rules: {
name: { required: true, minLength: 2 },
email: { required: true, email: true },
},
onSubmit: async values => {
await api.createAccount(values);
},
});
<FieldFlowInput
label="Name"
value={form.values.name}
onChangeText={form.handleChange('name')}
onBlur={form.handleBlur('name')}
errorText={form.touched.name ? form.errors.name : undefined}
/>;form.submit() validates every field (including async + cross-field rules) and only calls onSubmit if everything passes.
Cross-field & async validation
const form = useFieldFlowForm({
initialValues: { password: '', confirm: '', email: '' },
rules: {
password: { required: true, minLength: 8 },
confirm: { required: true },
email: { required: true, email: true },
},
crossFieldRules: [
{
target: 'confirm',
deps: ['password'],
validate: v =>
v.password === v.confirm
? { valid: true }
: { valid: false, message: 'Passwords must match' },
},
],
asyncValidators: {
email: async value => {
const taken = await api.isEmailTaken(value);
return taken
? { valid: false, message: 'Email is already taken' }
: { valid: true };
},
},
});
// form.validating.email === true while the async check runs
// form.isValidating === true if any async validator is in flightNote (0.4.0):
validateFieldandvalidateAllare nowasyncand returnPromise<boolean>.
Zod (optional)
npm install zodimport { z } from 'zod';
import { useFieldFlowForm, zodFormValidate } from 'rn-inputforge';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const form = useFieldFlowForm({
initialValues: { email: '', password: '' },
formValidate: zodFormValidate(schema),
onSubmit: async values => api.login(values),
});Also exported: zodToFormErrors, createZodSubmitGuard, fieldRulesFromZodString.
Try it (Expo / Snack)
- Create a blank Snack at snack.expo.dev.
- Add dependency
rn-inputforge(andreact-hook-formif you try the RHF adapter). - Paste any snippet from this README or copy
example/App.tsxfrom the repo.
Same install works in a local Expo app:
npx create-expo-app MyFormDemo
cd MyFormDemo
npx expo install rn-inputforgePassword strength
<FieldFlowInput
type="password"
label="Password"
value={password}
onChangeText={setPassword}
showStrength
// or: showStrength={{ showRules: true, rules: customRules }}
/>
// Standalone meter
<FieldFlowPasswordStrength password={password} />Pure helper: evaluatePasswordStrength(password) → { level, progress, passed, failed }.
Tags / chip input
const [skills, setSkills] = useState<string[]>([]);
<FieldFlowTags
label="Skills"
value={skills}
onChange={setSkills}
maxTags={8}
placeholder="Type a skill and press Enter"
helperText="Comma or Enter to add"
/>React Hook Form adapter
npm install react-hook-formimport { useForm } from 'react-hook-form';
import { FieldFlowController, FieldFlowTagsController } from 'rn-inputforge';
const { control, handleSubmit } = useForm({
defaultValues: { email: '', skills: [] as string[] },
});
<FieldFlowController
control={control}
name="email"
type="email"
label="Email"
suggestions
rules={{ required: 'Email is required' }}
/>
<FieldFlowTagsController
control={control}
name="skills"
label="Skills"
rules={{ validate: v => (v?.length ? true : 'Add at least one skill') }}
/>Also exported: mapRhfToFieldFlowProps for custom Controller renders.
Smart suggestion chips
Show quick-complete chips while a field is focused — a UX gap most RN input kits skip:
// Infer from type: email → @gmail.com…, phone → +1/+44…, number/decimal → $/€/£
<FieldFlowInput type="email" label="Email" value={email} onChangeText={setEmail} suggestions />
// Force a preset or pass a custom list
<FieldFlowInput type="phone" suggestions="phone" value={phone} onChangeText={setPhone} />
<FieldFlowInput suggestions={['@company.com', '@partner.io']} value={work} onChangeText={setWork} />
// Standalone bar (e.g. above the keyboard area)
<FieldFlowAffordance
kind="email"
value={email}
onSelect={setEmail}
/>Helpers exported: applyEmailDomainSuggestion, DEFAULT_EMAIL_DOMAINS, DEFAULT_PHONE_DIAL_CODES, DEFAULT_CURRENCY_SYMBOLS.
Form intelligence
Enable friction tracking and accessibility autopilot on the form hook:
const form = useFieldFlowForm({
initialValues: { email: '', password: '' },
rules: { email: { required: true, email: true }, password: { required: true, minLength: 8 } },
fieldOrder: ['email', 'password'],
intelligence: {
onReport: report => console.log(report.mostFriction),
// focusFirstError: true (default)
// announceErrors: true (default) — VoiceOver / TalkBack
},
onSubmit: async values => api.login(values),
});
<FieldFlowInput
ref={r => form.registerFieldRef('email', { current: r })}
label="Email"
{...form.getInputProps('email')}
suggestions
/>;
// After interactions:
form.intelligence?.getReport();
// → { sessionMs, submitAttempts, fields: { email: { focusCount, errorCount, totalFocusMs, … } }, mostFriction }Standalone helpers: useFormIntelligence, focusFirstError, announceForAccessibility.
Conditional fields & field arrays
const form = useFieldFlowForm({
initialValues: { accountType: 'personal', company: '', taxId: '' },
conditions: {
company: {
showWhen: v => v.accountType === 'business',
requiredWhen: v => v.accountType === 'business',
},
taxId: {
showWhen: v => v.accountType === 'business',
disabledWhen: v => v.accountType !== 'business',
},
},
});
{form.isVisible('company') && (
<FieldFlowInput label="Company" {...form.getInputProps('company')!} />
)}Repeatable groups:
import { useFieldFlowArray, FieldFlowInput } from 'rn-inputforge';
const guests = useFieldFlowArray({
createItem: () => ({ name: '', email: '' }),
min: 1,
max: 5,
});
{guests.items.map((guest, i) => (
<FieldFlowInput
key={i}
label={`Guest ${i + 1}`}
value={guest.name}
onChangeText={name => guests.update(i, g => ({ ...g, name }))}
/>
))}
<Button title="Add guest" disabled={guests.isMax} onPress={() => guests.append()} />Draft resume
Persist form values with any storage backend (AsyncStorage, MMKV, custom). No hard dependency:
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useFieldFlowDraft, FieldFlowDraftBanner, useFieldFlowForm } from 'rn-inputforge';
const form = useFieldFlowForm({ initialValues: { name: '', email: '' }, /* … */ });
const draft = useFieldFlowDraft({
key: 'signup-v1',
storage: {
getItem: k => AsyncStorage.getItem(k),
setItem: (k, v) => AsyncStorage.setItem(k, v),
removeItem: k => AsyncStorage.removeItem(k),
},
values: form.values,
onRestore: form.setValues,
exclude: ['password'], // never persist secrets
ttlMs: 7 * 24 * 60 * 60 * 1000,
});
<FieldFlowDraftBanner
visible={draft.hasDraft}
savedAt={draft.draftSavedAt}
onResume={draft.resume}
onDiscard={draft.discard}
/>;On successful submit, call await draft.clear() so the banner doesn't reappear.
Multi-step wizard
import { useFieldFlowForm, useFieldFlowSteps, FieldFlowStepper, FieldFlowInput } from 'rn-inputforge';
const form = useFieldFlowForm({
initialValues: { name: '', email: '', bio: '' },
rules: {
name: { required: true },
email: { required: true, email: true },
},
onSubmit: async values => api.signup(values),
});
const wizard = useFieldFlowSteps({
values: form.values,
validateFields: form.validateFields,
onComplete: form.submit,
steps: [
{ id: 'profile', title: 'Profile', fields: ['name'] },
{ id: 'contact', title: 'Contact', fields: ['email'] },
{ id: 'about', title: 'About', fields: ['bio'] },
],
});
<>
<FieldFlowStepper steps={wizard.steps} stepIndex={wizard.stepIndex} onStepPress={wizard.goTo} />
{wizard.step.id === 'profile' && <FieldFlowInput label="Name" {...form.getInputProps('name')!} />}
{wizard.step.id === 'contact' && <FieldFlowInput type="email" label="Email" {...form.getInputProps('email')!} />}
{wizard.step.id === 'about' && <FieldFlowInput label="Bio" {...form.getInputProps('bio')!} />}
<Button title={wizard.isLast ? 'Submit' : 'Next'} onPress={() => wizard.next()} />
{!wizard.isFirst && <Button title="Back" onPress={wizard.back} />}
</>;Server errors
Map API 422 responses straight into the form:
try {
await api.signup(form.values);
} catch (err) {
// { email: 'Already taken', password: 'Too weak' }
form.setErrors(err.fieldErrors);
// or: form.setFieldError('email', 'Already taken');
// form.clearErrors('email') / form.clearErrors()
}Also available: form.isDirty, form.dirtyFields, form.validateFields(['email', 'password']).
Localized validation
const form = useFieldFlowForm({
initialValues: { email: '' },
rules: { email: { required: true, email: true } },
locale: 'es', // en | es | fr | de | pt | ar
// or: messages: createValidationMessages({ required: 'Custom…' }, 'en'),
});Helpers: getValidationMessages('fr'), validationMessageCatalogs, createValidationMessages.
Dirty leave guard
import { useFieldFlowDirtyGuard } from 'rn-inputforge';
const { confirmIfDirty } = useFieldFlowDirtyGuard({
isDirty: form.isDirty,
onDiscard: form.reset,
});
// React Navigation:
navigation.addListener('beforeRemove', async e => {
if (!form.isDirty) return;
e.preventDefault();
if (await confirmIfDirty()) navigation.dispatch(e.data.action);
});Android hardware back is intercepted automatically when dirty.
Phone with country code
import { FieldFlowPhone } from 'rn-inputforge';
const [phone, setPhone] = useState(null);
<FieldFlowPhone
label="Mobile"
value={phone}
onChange={setPhone}
defaultCountryCode="PK"
required
/>;
// phone.e164 → "+923001234567"File / image upload
Pure JS — you inject the picker (Expo or RN):
import * as ImagePicker from 'expo-image-picker';
import { FieldFlowUpload } from 'rn-inputforge';
<FieldFlowUpload
label="ID photo"
maxFiles={2}
value={files}
onChange={setFiles}
pick={async () => {
const res = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'] });
if (res.canceled || !res.assets[0]) return null;
const a = res.assets[0];
return { uri: a.uri, name: a.fileName ?? 'photo.jpg', mimeType: a.mimeType, size: a.fileSize };
}}
/>;Async typeahead
import { FieldFlowTypeahead } from 'rn-inputforge';
<FieldFlowTypeahead
label="City"
value={city}
displayValue={cityLabel}
onChange={(value, option) => {
setCity(value);
setCityLabel(option?.label ?? '');
}}
fetchOptions={async query => {
const rows = await api.searchCities(query);
return rows.map(r => ({ label: r.name, value: r.id }));
}}
minChars={2}
/>;Rating / slider / number stepper
<FieldFlowRating label="Experience" value={rating} onChange={setRating} />
<FieldFlowSlider label="Budget" value={budget} onChange={setBudget} min={0} max={1000} step={50} />
<FieldFlowNumberStepper label="Guests" value={guests} onChange={setGuests} min={1} max={12} />Credit card group
import { FieldFlowCreditCard } from 'rn-inputforge';
const [card, setCard] = useState({ number: '', expiry: '', cvc: '', name: '' });
<FieldFlowCreditCard value={card} onChange={setCard} showName required />;Signature pad
import { FieldFlowSignature } from 'rn-inputforge';
<FieldFlowSignature
label="Sign to confirm"
value={signature}
onChange={setSignature}
required
/>;
// signature.isEmpty / signature.strokesVariants
Every text-like field (FieldFlowInput, FieldFlowMaskedInput, date picker trigger) supports:
| variant | Look |
| ------------ | ----------------------------------------- |
| outlined | Bordered box (default) |
| filled | Soft filled background |
| underline | Bottom border only |
| floating | Label animates into the border when filled / focused |
<FieldFlowInput variant="floating" label="Email" type="email" value={email} onChangeText={setEmail} />
<FieldFlowInput variant="underline" label="Name" leftIcon="user" value={name} onChangeText={setName} />Icons
Built-in, dependency-free icons — pass a name or any custom React node:
<FieldFlowInput leftIcon="mail" rightIcon={<MyLucideIcon />} />
<FieldFlowInput type="email" /> // auto leftIcon="mail"
<FieldFlowInput type="password" /> // auto leftIcon="lock" + show/hide
<FieldFlowMaskedInput mask="card" /> // auto leftIcon="creditCard"Built-in names include: eye, eyeOff, clear, check, alert, search, send, camera, image, paperclip, mic, mapPin, chevronDown/Up/Left/Right, calendar, clock, creditCard, currency, phone, mail, lock, user, key, hash, otp, radio, radioFilled, switch, star, plus, minus, info, home, settings, edit, trash, link, globe, file, folder, bell, heart, spinner.
Use FieldFlowIcon / renderFieldFlowIcon anywhere in your app. Set iconSize on components to scale them. Pass leftIcon={null} / undefined carefully — omit the prop to keep defaults, or pass a custom node to replace.
OTP / PIN
import { FieldFlowOTP } from 'rn-inputforge';
<FieldFlowOTP
label="Verification code"
leftIcon="otp"
length={6}
value={code}
onChangeText={setCode}
onComplete={value => verify(value)}
cellSize={48}
/>Supports paste/autofill (textContentType="oneTimeCode"), backspace navigation, secure mode, custom renderCell, cell styles, and icons.
Masked inputs
import { FieldFlowMaskedInput } from 'rn-inputforge';
<FieldFlowMaskedInput mask="phone" label="Phone" value={phone} onChangeText={setPhone} />
<FieldFlowMaskedInput mask="card" label="Card" valueMode="raw" value={card} onChangeText={(_, raw) => setCard(raw)} />
<FieldFlowMaskedInput mask="currency" maskOptions={{ currencySymbol: '$' }} value={amount} onChangeText={setAmount} />
<FieldFlowMaskedInput mask="date" label="Date" value={date} onChangeText={setDate} />
<FieldFlowMaskedInput mask="custom" maskOptions={{ pattern: '###-##-####' }} value={ssn} onChangeText={setSsn} />Built-in masks: phone, card, currency, date, custom (pattern with # = digit). onChangeText(formatted, raw) always receives both; valueMode controls which string you store ('formatted' default, or 'raw').
Low-level helpers are also exported: applyMask, formatMasked, unmask, digitsOnly.
Date & time picker
Pure JS calendar / time sheet (no native modules):
import { FieldFlowDateTimePicker } from 'rn-inputforge';
<FieldFlowDateTimePicker mode="date" label="Birthday" value={dob} onChange={setDob} clearable />
<FieldFlowDateTimePicker mode="time" label="Start time" value={time} onChange={setTime} hourCycle={12} />
<FieldFlowDateTimePicker
mode="datetime"
label="Appointment"
value={appt}
onChange={setAppt}
leftIcon="calendar"
minimumDate="2020-01-01"
formatDisplay={(d) => d.toLocaleString()}
/>Fully customizable: icons, labels, confirm/cancel text, renderTrigger, formatDisplay, min/max dates, theme, styles.
Checkbox, radio & switch
import { FieldFlowCheckbox, FieldFlowRadio, FieldFlowSwitch } from 'rn-inputforge';
<FieldFlowCheckbox
label="I agree to the Terms"
leftIcon="file"
checkedIcon="check"
value={accepted}
onChange={setAccepted}
required
/>
<FieldFlowRadio
label="Plan"
leftIcon="star"
value={plan}
onChange={setPlan}
options={[
{ label: 'Starter', value: 'starter', icon: 'home' },
{ label: 'Pro', value: 'pro', description: 'Most popular', icon: 'star' },
]}
/>
<FieldFlowSwitch label="Push notifications" leftIcon="bell" value={pushOn} onChange={setPushOn} />Override colors (checkedColor, onTrackColor, …), icons, sizes, and renderOption when you need full control.
Keyboard & form navigation
useKeyboardManager handles "Next" → focus the next field, "Done" → submit, and keeps the focused field scrolled above the keyboard:
const keyboardManager = useKeyboardManager({
order: ['name', 'email', 'password'],
onSubmit: form.submit,
});
<KeyboardAwareFormWrapper keyboardManager={keyboardManager}>
<FieldFlowInput
ref={r => keyboardManager.registerField('name', { current: r })}
returnKeyType={keyboardManager.getReturnKeyType('name')}
onSubmitEditing={() => keyboardManager.focusNext('name')}
/* ...value, onChangeText, etc. */
/>
{/* ...more fields */}
</KeyboardAwareFormWrapper>;See example/App.tsx for a complete multi-field form wired end to end.
Dropdown / select (single & multiple)
import { FieldFlowDropdown } from 'rn-inputforge';
// Single select — slides up from the bottom by default
<FieldFlowDropdown
label="Country"
placeholder="Select your country"
options={[
{ label: 'United States', value: 'us' },
{ label: 'Pakistan', value: 'pk' },
{ label: 'United Kingdom', value: 'uk' },
]}
value={country}
onChange={setCountry}
clearable
required
/>
// Multi select — centered popup modal, with search, confirmed via a "Done" button
<FieldFlowDropdown
multiple
label="Skills"
presentation="popup"
options={skillOptions}
values={skills}
onChangeMulti={setSkills}
searchable
/>Presentation modes (presentation prop):
"bottomSheet"(default) — slides up from the bottom, dismiss by tapping the backdrop"popup"— fades in, centered on screen"inline"— drops down directly under the field itself (flips upward automatically if there isn't room below)
Single vs multiple select: pass value + onChange for single select, or multiple + values + onChangeMulti for multi-select. In multi-select, selections are staged until the user taps the confirm button (confirmLabel, default "Done") — pass closeOnSelect if you'd rather commit immediately on each tap instead.
Search is shown automatically once there are more than searchThreshold options (default 6) — set searchable explicitly to force it on or off. Every option can carry a description (shown as a second line) and can be individually disabled. Like every other FieldFlow component, it accepts theme, label, helperText, errorText, and disabled for consistency with the rest of your form.
Description fields
import { DescriptionField } from 'rn-inputforge';
<DescriptionField
label="Bio"
placeholder="Tell us about yourself..."
value={bio}
onChangeText={setBio}
maxLength={240}
minHeight={80}
maxHeight={200}
/>;Grows automatically as the user types, shows a live character counter, and supports every validation/theming prop FieldFlowInput does.
Chat input
import { ChatInput } from 'rn-inputforge';
<ChatInput
value={message}
onChangeText={setMessage}
onSend={text => sendMessage(text)}
attachmentActions={['camera', 'gallery', 'document', 'location']}
onAttachmentPress={action => handleAttachment(action)}
customActions={[{ id: 'gif', icon: 'image', onPress: openGifPicker }]}
placeholder="Type a message..."
/>;Handles expandable height, keyboard-avoiding behavior, and a send button that's only active once there's text to send. Attachment buttons are opt-in per screen — pass only the ones your chat needs.
Autofill profiles
import { FieldFlowInput, autofillProfiles } from 'rn-inputforge';
<FieldFlowInput type="email" label="Email" autofill={autofillProfiles.signup.email} ... />
<FieldFlowInput type="password" label="Password" autofill="newPassword" ... />
<FieldFlowOTP autofill="oneTimeCode" ... /> // or pass through TextInput propsRoles: email, password, newPassword, oneTimeCode, tel, name, streetAddress, ccNumber, … — see getAutofillProps / autofillProfiles.
Keyboard toolbar
import {
FieldFlowKeyboardToolbar,
FIELD_FLOW_KEYBOARD_TOOLBAR_ID,
useKeyboardManager,
} from 'rn-inputforge';
const kb = useKeyboardManager({ order: ['email', 'password'], onSubmit: form.submit });
<>
<FieldFlowInput
inputAccessoryViewID={FIELD_FLOW_KEYBOARD_TOOLBAR_ID}
...
/>
<FieldFlowKeyboardToolbar
nativeID={FIELD_FLOW_KEYBOARD_TOOLBAR_ID}
onPrevious={() => kb.focusPrevious(activeId)}
onNext={() => kb.focusNext(activeId)}
onDone={form.submit}
canPrevious={kb.canGoPrevious(activeId)}
canNext={kb.canGoNext(activeId)}
/>
</>Smart paste
import { routeSmartPaste, useSmartPaste } from 'rn-inputforge';
const { handlePaste } = useSmartPaste({
targets: {
otp: r => setCode(r.value),
card: r => setCardNumber(r.raw ?? r.value),
email: r => form.handleChange('email')(r.value),
addressFields: addr => {
form.handleChange('street')(addr.street ?? '');
form.handleChange('city')(addr.city ?? '');
},
},
});
// After Clipboard.getString():
handlePaste(pastedText);Completeness coach
import { useFieldFlowCompleteness, FieldFlowCompleteness } from 'rn-inputforge';
const completeness = useFieldFlowCompleteness({
values: form.values,
requiredFields: ['name', 'email', 'password'],
});
<FieldFlowCompleteness completeness={completeness} />;Auth actions
import { FieldFlowAuthActions } from 'rn-inputforge';
<FieldFlowAuthActions
actions={[
{ id: 'passkey', label: 'Passkey', icon: 'key', onPress: signInWithPasskey },
{ id: 'apple', label: 'Apple', icon: 'user', onPress: signInWithApple },
{ id: 'google', label: 'Google', icon: 'globe', onPress: signInWithGoogle },
]}
/>Schema form renderer
import { FieldFlowSchemaForm, type FieldFlowFormSchema } from 'rn-inputforge';
const schema: FieldFlowFormSchema = {
fields: [
{ name: 'name', type: 'text', label: 'Name', required: true, section: 'profile', autofill: 'name' },
{ name: 'email', type: 'email', label: 'Email', required: true, section: 'profile' },
{ name: 'plan', type: 'segmented', label: 'Plan', options: [
{ label: 'Free', value: 'free' },
{ label: 'Pro', value: 'pro' },
]},
],
sections: [{ id: 'profile', title: 'Profile', icon: 'user' }],
};
<FieldFlowSchemaForm
schema={schema}
onSubmit={async values => api.save(values)}
renderFooter={({ submit, isSubmitting }) => (
<Button title={isSubmitting ? '…' : 'Save'} onPress={() => void submit()} />
)}
/>;A11y auditor
import { FieldFlowA11yAudit, runA11yAudit } from 'rn-inputforge';
<FieldFlowA11yAudit
audit={{
fields: [
{ name: 'email', label: 'Email', required: true, hasAccessibilityLabel: true },
{ name: 'bio', placeholderOnly: true, hitHeight: 36 },
],
respectsReducedMotion: true,
}}
/>;Dev-only by default (devOnly).
Offline submit queue
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useFieldFlowSubmitQueue } from 'rn-inputforge';
const queue = useFieldFlowSubmitQueue({
key: 'signup-queue',
storage: AsyncStorage,
submit: async payload => {
await api.signup(payload);
},
});
// Instead of failing hard when offline:
await queue.enqueue(form.values);
// queue.pendingCount / queue.flush() / queue.clear()Custom render slots
Most controls accept optional render props so apps can keep FieldFlow behavior and swap chrome:
| Component | Slots |
| --------- | ----- |
| FieldFlowUpload | renderDropzone, renderFile |
| FieldFlowTypeahead | renderOption, renderEmpty |
| FieldFlowRating | renderStar |
| FieldFlowSlider | renderThumb |
| FieldFlowNumberStepper | renderDecrease, renderIncrease, renderValue |
| FieldFlowPhone | renderDialButton, renderCountryOption |
| FieldFlowStepper | renderStep, headerRight |
| FieldFlowSignature | renderClear, renderPlaceholder |
<FieldFlowRating
value={rating}
onChange={setRating}
renderStar={({ filled, size }) => (
<Text style={{ fontSize: size }}>{filled ? '🟢' : '⚪'}</Text>
)}
/>
<FieldFlowUpload
pick={myPicker}
value={files}
onChange={setFiles}
renderFile={({ file, remove }) => (
<Pressable onPress={remove}>
<Text>{file.name} ✕</Text>
</Pressable>
)}
/>Defaults stay unchanged when you omit these props.
Visual system
One-liner knobs on FieldFlowThemeProvider — easy on the eyes, fully customizable:
<FieldFlowThemeProvider
theme={themePresets.soft}
density="comfortable" // compact | comfortable | spacious
size="md" // sm | md | lg
shape="rounded" // square | rounded | pill
surface="soft" // flat | elevated | soft | inset
focusRing="softGlow" // solid | softGlow | underlinePulse
animateTheme // soft crossfade when theme changes
>
<App />
</FieldFlowThemeProvider>Inline alert / skeleton / form header
<FieldFlowFormHeader
title="Create account"
subtitle="A calm start — we’ll guide you through a few short steps."
progress={0.35}
/>
<FieldFlowInlineAlert tone="info" title="Privacy" message="We’ll never share your email." />
{loading ? <FieldFlowSkeleton count={4} lastMultiline /> : <FormFields />}Info tip + helper status
<FieldFlowInput
label="Tax ID"
infoTip="Used only for invoices. Never shown publicly."
helperText="Optional for personal accounts"
helperStatus="info"
...
/>Layout & form chrome
UI building blocks added in 0.5.2 — pair with the visual system for polished signup / checkout / profile screens.
<FieldFlowToastProvider>
<FieldFlowBannerStack
banners={[{ id: '1', tone: 'warning', message: 'Verify your email to unlock exports.' }]}
/>
<FieldFlowOrGate
above={<FieldFlowAuthActions actions={ssoActions} dividerLabel="" />}
label="or continue with email"
below={<EmailPasswordFields />}
/>
<FieldFlowSplitRow weights={[1, 1]}>
<FieldFlowInput label="First name" ... />
<FieldFlowInput label="Last name" ... />
</FieldFlowSplitRow>
<FieldFlowAddress value={address} onChange={setAddress} />
<FieldFlowMoney label="Budget" currencySymbol="$" value={amount} onChangeText={setAmount} />
<FieldFlowPercent label="Discount" value={pct} onChangeText={setPct} />
<FieldFlowColor
label="Brand color"
value={color}
onChange={setColor}
options={[
{ value: '#3B82F6' },
{ value: '#22C55E' },
{ value: '#EF4444' },
]}
/>
<FieldFlowAvatarPicker initials="SW" onPress={openImagePicker} />
<FieldFlowBottomSheetField label="Country" valueLabel={country}>
{({ close }) => <CountryList onPick={c => { setCountry(c); close(); }} />}
</FieldFlowBottomSheetField>
<FieldFlowConsent
label="I agree to the Terms"
value={agreed}
onChange={setAgreed}
links={[{ label: 'Terms', url: 'https://example.com/terms' }]}
/>
<FieldFlowTermsGate terms={LONG_TERMS} value={accepted} onChange={setAccepted} />
<FieldFlowReviewSummary
items={[
{ id: 'email', label: 'Email', value: email, section: 'Account', onEdit: () => goToStep(0) },
{ id: 'plan', label: 'Plan', value: 'Pro', section: 'Billing' },
]}
/>
<FieldFlowEmptyState kind="noResults" actionLabel="Clear filters" onAction={clear} />
</FieldFlowToastProvider>Morph, float presets, haptics
<FieldFlowInput
variant="floating"
floatPreset="material" // subtle | dramatic | material
morph // error pulse + success morph (default on with motion)
haptics // light Vibration on focus / success / error
themeCrossfade // soft fade when this field’s theme prop changes
...
/>
const { onTick } = useFieldFlowHaptics(); // or inject expo-haptics via `trigger`Motion & UI
Field motion
FieldFlowInput ships with micro-interactions on by default (motion={true}):
- Focus glow — soft ring around the field chrome
- Error shake — brief horizontal shake when validation fails
- Message fade — helper / error text cross-fades in
- Success pop — right-side success icon scales in
All of this uses React Native Animated (no Reanimated) and respects OS Reduce Motion. Pass motion={false} to disable per field, or use useReducedMotion / useFieldMotion if you build custom chrome.
<FieldFlowInput label="Email" type="email" value={email} onChangeText={setEmail} motion />Prefix & suffix
Inline affixes sit inside the field before / after the value (alongside icons):
<FieldFlowInput
type="phone"
label="Phone"
prefix="+92"
value={phone}
onChangeText={setPhone}
/>
<FieldFlowInput
type="decimal"
label="Weight"
suffix="kg"
value={weight}
onChangeText={setWeight}
/>Form sections
Group related fields into a themed card with optional collapse:
import { FieldFlowSection, FieldFlowInput } from 'rn-inputforge';
<FieldFlowSection title="Account" description="How we’ll reach you" icon="mail" collapsible>
<FieldFlowInput type="email" label="Email" value={email} onChangeText={setEmail} />
<FieldFlowInput type="phone" label="Phone" prefix="+1" value={phone} onChangeText={setPhone} />
</FieldFlowSection>;Segmented control
Animated pill control for plan pickers, account type, filters:
import { FieldFlowSegmented } from 'rn-inputforge';
<FieldFlowSegmented
label="Account type"
value={accountType}
onChange={setAccountType}
options={[
{ label: 'Personal', value: 'personal', icon: 'user' },
{ label: 'Business', value: 'business', icon: 'globe' },
]}
/>;Theming
Wrap your app (or just a screen) in FieldFlowThemeProvider and override only what you need — everything else falls back to the default theme:
import { FieldFlowThemeProvider, themePresets } from 'rn-inputforge';
// One-liner brand start
<FieldFlowThemeProvider theme={themePresets.soft}>
<App />
</FieldFlowThemeProvider>
// Or merge your own tokens
<FieldFlowThemeProvider
theme={{
colors: { borderFocused: '#7C3AED', labelFocused: '#7C3AED' },
spacing: { borderRadius: 14 },
}}
>
<App />
</FieldFlowThemeProvider>;Bundled presets: default, dark, soft, warm, teal, midnight, contrast. Use getThemePreset('teal') if you prefer a function call.
You can also pass a one-off theme prop directly to any single field to override it locally, or use the bundled darkTheme as a starting point for dark mode.
Theme tokens you can override: colors.*, spacing.*, typography.*, animation.* (see FieldFlowTheme in the package types).
Public API at a glance
Import anything below from 'rn-inputforge':
// Components
FieldFlowInput, FieldFlowOTP, FieldFlowMaskedInput, FieldFlowDateTimePicker,
FieldFlowCheckbox, FieldFlowRadio, FieldFlowSwitch, FieldFlowDropdown,
FieldFlowTags, FieldFlowSegmented, FieldFlowSection, FieldFlowAffordance,
FieldFlowDraftBanner, FieldFlowPasswordStrength, FieldFlowStepper,
FieldFlowPhone, FieldFlowUpload, FieldFlowTypeahead, FieldFlowRating,
FieldFlowSlider, FieldFlowNumberStepper, FieldFlowCreditCard, FieldFlowSignature,
DescriptionField, ChatInput, KeyboardAwareFormWrapper
// Form hooks
useFieldFlowForm, useFieldFlowArray, useFieldFlowDraft,
useFieldFlowSteps, useFieldFlowDirtyGuard,
useKeyboardManager, useFieldState, useFormIntelligence
// Motion / Theming / i18n
useFieldMotion, useReducedMotion
FieldFlowThemeProvider, themePresets, getThemePreset, defaultTheme, darkTheme
getValidationMessages, createValidationMessages, validationMessageCatalogs
// Adapters / Icons
FieldFlowController, FieldFlowTagsController, mapRhfToFieldFlowProps
FieldFlowIcon, renderFieldFlowIcon
// Validation, masks, phone, a11y
runRules, zod helpers, applyMask, unmask, DEFAULT_COUNTRY_DIALS,
evaluatePasswordStrength, focusFirstError, announceForAccessibility, …Types for props, themes, rules, and draft storage are exported alongside the values. See example/App.tsx for a full usage reference screen.
Project structure
rn-inputforge/
├── src/
│ ├── components/
│ │ ├── FieldFlowInput/ core input + variants + motion + affixes
│ │ ├── FieldFlowSection/ themed form card / collapse
│ │ ├── FieldFlowSegmented/ animated pill control
│ │ ├── FieldFlowAffordance/ standalone suggestion chip bar
│ │ ├── FieldFlowOTP/ OTP / PIN digit cells
│ │ ├── FieldFlowMaskedInput/ phone, card, currency, date, custom masks
│ │ ├── FieldFlowDateTimePicker/ date / time / datetime (pure JS)
│ │ ├── FieldFlowCheckbox/ themed checkbox
│ │ ├── FieldFlowRadio/ radio group
│ │ ├── FieldFlowSwitch/ animated switch
│ │ ├── FieldFlowDraftBanner/ resume-draft UI
│ │ ├── FieldFlowTags/ multi-value chip input
│ │ ├── FieldFlowPasswordStrength/ strength meter + rules
│ │ ├── DescriptionField/ auto-growing multiline field
│ │ ├── ChatInput/ chat composer + attachment toolbar
│ │ ├── FieldFlowDropdown/ single/multi select
│ │ └── KeyboardAwareFormWrapper/
│ ├── adapters/ react-hook-form Controllers
│ ├── hooks/ form, array, draft, intelligence, keyboard
│ ├── intelligence/ a11y helpers + friction report types
│ ├── motion/ field motion + reduce-motion hooks
│ ├── theme/ ThemeProvider + presets + default/dark
│ ├── validation/ validators, Zod adapter
│ ├── icons/ dependency-free icon set
│ ├── utils/ masks, affordance, password strength, …
│ ├── __tests__/ Jest unit tests
│ └── index.ts public API
├── example/App.tsx full usage reference
├── package.json
└── tsconfig*.jsonBuilding the library locally
The package is set up with react-native-builder-bob to produce CommonJS, ES module, and TypeScript declaration output — the standard setup for RN libraries, and it works the same regardless of whether the consuming app uses npm or Yarn.
# with yarn
yarn install
yarn build
# with npm
npm install
npm run buildThis produces:
lib/commonjs/ -> require()-style output
lib/module/ -> ES module output (used by Metro when bundling)
lib/typescript/ -> .d.ts declaration filesPublishing
npm version patch # or minor / major
npm publish # or: yarn publishBoth package managers read the same package.json, so there's nothing extra to configure to support either one.
License
MIT
