Skip to content
Back to skills

Forms And Input

ASecurity

Builds react-hook-form + valibot forms: one schema, catalog-key errors, TextField kinds (keyboard, autofill), focus chain. Use when adding a form, text field or validation, or the keyboard hides a field. Not for sign-in (auth-and-secure-storage).

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 18, 2026
developmentgoreactnodegitapi

Works with

  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 19 files and shows the line behind each finding

Scanned October 18, 2026

npx -y skills add zakariaf/ReactNative-Skills --skill forms-and-input --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Forms And Input?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Forms And Input
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-forms-and-input-reactnative-skills/badge)](https://www.skillsdirectory.com/skills/zakariaf-forms-and-input-reactnative-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: forms-and-input
description: "Builds react-hook-form + valibot forms: one schema, catalog-key errors, TextField kinds (keyboard, autofill), focus chain. Use when adding a form, text field or validation, or the keyboard hides a field. Not for sign-in (auth-and-secure-storage)."
---

# Forms and input

Makes every form in an Expo app one valibot schema behind react-hook-form, whose every message is a
catalog key, rendered with one labelled `TextField` whose kind sets the keyboard, autofill and
capitalization, chained by the return key, kept above the keyboard on iOS and Android, and checked
asynchronously only outside the schema. One zero-dependency checker proves it on the app's source.

## Rules that must hold

1. **One valibot schema per form, behind `valibotResolver`.** `useForm<Input, unknown, Output>({
   resolver: valibotResolver(schema) })`; the field types are `v.InferInput`, the submitted draft is
   `v.InferOutput`; no `rules` on a Controller (`form-resolver`). Why: one definition gives the type,
   the runtime check and every message, so a field can never validate one way and save another.
2. **Every message is a catalog key, translated at the field.** Each validation action carries a
   literal key of the source catalog (`v.nonEmpty('task-editor.title.required')`,
   `schema-message-key`); `setError` passes a key too (`error-message-key`); `fieldMessageKey` turns
   anything else into the generic key. Why: an action without a message shows valibot's English
   default ("Invalid length: Expected !0 but received 0") in every language.
3. **The schema is synchronous and Hermes-safe.** No `*Async` API in a form schema (`schema-async`);
   no graphemes or words actions unless `Intl.Segmenter` is polyfilled (`schema-segmenter`). Why: the
   resolver runs the whole schema on every validation, with no pause, cancel or deadline, and Hermes
   has no `Intl.Segmenter` (it throws on the phone while Jest on Node passes).
4. **An async check ("is this name free?") runs in `useAsyncFieldCheck`, never in the schema.** It
   starts only for a valid shape, waits for typing to pause, lets the newest value win, has a
   deadline, stops on unmount, and says which value it answers; submit needs a pass for exactly the
   value sent, and the server still judges at write time (a refusal becomes `setError` with a key).
   Why: a check per keystroke floods the server and an old answer can land on a newer value.
5. **Digits in any script are read.** A schema that matches `\d` or calls `Number()` transforms the
   text with `toLatinDigits` first, or, for a decimal, money or quantity field, with
   value-objects-money-and-units' `normalizeDecimalInput` (`latin-digits`). Why: Arabic, Persian and
   Devanagari keyboards, and the iOS pads in those languages, type ٣٠ or ۳۰ (and `٫` or `,` for the
   decimal point), which `/^\d+$/` and `Number()` refuse.
6. **Screens render `TextField`, never a raw `TextInput`** (`raw-text-input`). `TextInput` lives only
   in the field folder (`src/ui/form/`), whose components join the component catalogue. Why: label,
   error, hint, live region, keyboard and autofill are solved once; a raw input forgets one of them.
7. **The visible label is the input's accessible name; the error is never colour alone.** The label
   sits above the input; on iOS the input carries it as `accessibilityLabel` (the label view hidden
   from VoiceOver), on Android it points at the label view with `accessibilityLabelledBy` (an
   EditText's content description loses to its typed text). The error or hint is the input's
   `accessibilityHint` and sits in a polite live region, and an invalid field gets a bold edge and a
   sentence (`input-label`). Why: a placeholder vanishes when typing starts and is not read as a
   name, and about 1 in 12 men cannot tell the error colour apart.
8. **A field kind sets the keyboard, autofill, capitalization, autocorrect and secure entry
   together** (`FIELD_KINDS`; a raw input that hides text sets `autoComplete`, an email keyboard sets
   `autoCapitalize`: `input-preset`). Why: a password without `autoComplete` gets no password manager
   on either platform, and an email field that capitalizes breaks every address.
9. **Fields bind the React Native way: `onChangeText`, `onBlur`, `ref`** (`useTextField`; never
   `onChange={field.onChange}`, `{...field}` or `register`: `field-on-change`; the ref is passed on:
   `field-ref`). Why: react-hook-form reads `event.target.value`, which a React Native change event
   lacks, so the field would hold `undefined`; without the ref nothing can focus the field.
10. **Errors wait for the first blur, and a refused submit goes to the first problem.** `mode:
    'onTouched'` (`form-mode`), `shouldFocusError` left on (`focus-error`), and the refusal is
    announced. Why: `onChange` mode shouts "invalid email" at the first letter; focusing the field
    moves VoiceOver and TalkBack there too.
11. **The return key walks the form.** A single-line field names the next field (`enterKeyHint`
    `next`, `submitBehavior` `submit`, `setFocus`), the last one submits (`done`), a multiline field
    makes new lines. Why: closing and reopening the keyboard between fields flickers and loses the
    user's place.
12. **The keyboard never hides the field or swallows a tap.** `KeyboardProvider` wraps the root
    (`keyboard-provider`); forms scroll in `KeyboardAwareScrollView` (`keyboard-aware-scroll`) with
    `keyboardShouldPersistTaps="handled"` (`keyboard-persist-taps`); a feature with a number,
    decimal or phone pad renders `KeyboardDoneBar` (`pad-done-bar`), and the scroll view keeps the
    focused field above the bar (`useDoneBarSpace`: `bottomOffset` = the bar's measured height plus
    a gap). Why: Android's edge-to-edge window no longer resizes for the keyboard, the first tap on
    Save would only close it, the iOS pads have no return key, and the bar sits on the keyboard,
    over whatever the scroll view keeps just above it.
13. **Submit is one void intent that never loses input.** The model calls `handleSubmit(...)()` and
    catches its promise, shows `isSubmitting` (from `useFormState`) as busy, resets the form only
    after the save returned `ok` and `handleSubmit` has settled, and keeps every typed character on
    a failure. Why: `handleSubmit` rethrows what the save throws, a reset before the write is data
    loss, and a reset inside the save is undone (the form stays "submitted" and the next draft
    shows errors from its first keystroke).

## Workflow

1. **Look first.** From the repo root run `node ${CLAUDE_SKILL_DIR}/scripts/check-forms.mjs .`.
   NOT APPLICABLE means the app has no form yet; otherwise the problems are the work list. Read
   [references/checks-and-tests.md](references/checks-and-tests.md) for each rule's fix.
2. **Prerequisites.** These come from their owner skills and must exist first: the theme, `AppText`
   and `PressableSurface` (design-system-structure), `t()`/`useT`, `MessageKey` and the catalogs
   (i18n-strings-and-catalogs), `useServices` and the error log (architecture-and-boundaries),
   `Result` (error-handling-typed-results), the latest-request, `delayOrAbort` and `withTimeout`
   helpers (async-safety), `useAnnounce`/`useReduceMotion` (accessibility-as-code), the render
   helpers (unit-and-component-tests). Install, verified 2026-10-03 (re-verify:
   `npm view react-hook-form version`): `npx expo install react-native-keyboard-controller`
   (1.21.9, in the SDK 57 map, included in Expo Go) and
   `npm install -E react-hook-form@7.89.0 @hookform/resolvers@5.9.1 valibot@1.5.0`.
3. **Copy the field kit** from `templates/` to the path on each file's first line: `src/ui/form/`
   (`field-kinds.ts`, `text-field.tsx`, `field-note.tsx`, `keyboard-done-bar.tsx`), `src/hooks/`
   (`use-text-field.ts`, `field-message-key.ts`, `use-async-field-check.ts`,
   `use-done-bar-space.ts`),
   `src/core/text/latin-digits.ts`, `test/forms/i18n-form-wrapper.tsx`, each with its test; merge
   the `forms.*` keys of `templates/src/i18n/catalogs/*.json` into every catalog and translate them.
   `__mocks__/react-native-keyboard-controller.ts` is the canonical root mock (keep it as is).
4. **Wire the root.** Wrap the root stack in `KeyboardProvider`, inside `GestureHandlerRootView`
   and outside the motion and navigation-theme providers
   ([examples/root-stack-keyboard-provider.tsx](examples/root-stack-keyboard-provider.tsx)). The
   canonical `root-stack.tsx` of app-startup-and-bootstrap already renders it: check it is there; in an
   older root, add the import and the element and keep the file's header (the `device-only` marker).
5. **List the field components.** If `contracts/component-library.json` does not exist yet, create
   it from ui-component-library's catalogue template first (with `src/ui/form/` in place, that
   skill's `check-components` fails `catalogue-missing` without it), then add the three entries of
   `assets/component-library-entries.json`.
6. **Write the schema first, with its test.** Copy the `task-editor` templates as the pattern and
   rename them: bounds from the data layer, a key per message, `toLatinDigits` before digits
   (`normalizeDecimalInput` for a decimal, money or quantity field), local
   duplicate checks through a factory. Read
   [references/schemas-and-messages.md](references/schemas-and-messages.md).
7. **Write the model hook**: `useForm` with the resolver and `mode: 'onTouched'`, one `useTextField`
   per field with its kind and `next`/`onSubmit`, the submit intent, labels translated
   ([references/fields-and-autofill.md](references/fields-and-autofill.md),
   [references/keyboard-focus-and-submit.md](references/keyboard-focus-and-submit.md)).
8. **Add an async check only when the answer lives elsewhere** (a server, another store): a
   `FieldCheck` port, `useAsyncFieldCheck`, the status and error on the field, submit gated on
   `hasPassed(value)` ([references/async-checks-and-server-errors.md](references/async-checks-and-server-errors.md),
   [examples/list-name-availability.ts](examples/list-name-availability.ts)).
9. **Write the screen** as a pure view: `KeyboardAwareScrollView` with
   `keyboardShouldPersistTaps="handled"`, one `TextField` per field spread from the model, the save
   failure in a live region, `KeyboardDoneBar` when a pad kind is used, its `onHeightChange` and the
   scroll view's `bottomOffset`/`extraKeyboardSpace` from the model's `useDoneBarSpace()`.
10. **Test**: the schema (every message is a key in every catalog), the model (focus, refused submit,
    failed save keeps input, digits), the screen by label and role ending in the pressable audit
    ([references/checks-and-tests.md](references/checks-and-tests.md)).
11. **Run the checks**: run, fix, rerun until `RESULT: PASS`. Then run the neighbours' checkers
    named in Related skills (component catalogue, accessibility, catalogs).

## Definition of done

- [ ] Every form has one valibot schema behind `valibotResolver`; its test proves every message it
      can produce is a key in every catalog.
- [ ] Screens render `TextField` with a kind; `TextInput` appears only in `src/ui/form/`.
- [ ] Each field shows a visible label, its error under it after the first blur, and the return key
      moves to the next field or submits.
- [ ] Async checks go through `useAsyncFieldCheck`; submit requires `hasPassed(value)`; server
      refusals become `setError` with a catalog key.
- [ ] `KeyboardProvider` wraps the root; forms scroll in `KeyboardAwareScrollView` with
      `keyboardShouldPersistTaps="handled"`; pad fields have `KeyboardDoneBar`, and the scroll
      view's `bottomOffset` comes from `useDoneBarSpace` (the screen test asserts it).
- [ ] A failed save keeps the input; only a saved draft resets the form, after `handleSubmit`
      settled.
- [ ] `contracts/component-library.json` exists and lists `TextField`, `FieldNote` and
      `KeyboardDoneBar`.
- [ ] Tested by hand once on an iOS simulator and an Android emulator, each named for the job: the
      focused field stays above the keyboard and the Done bar (also at the largest text size), Next
      walks the fields, autofill offers the right entries, and VoiceOver and TalkBack read each
      field's label, empty and filled (record the result in fields-and-autofill.md).
- [ ] `node ${CLAUDE_SKILL_DIR}/scripts/check-forms.mjs .` prints `RESULT: PASS` (or NOT APPLICABLE
      before the first form). The only SKIP it may print is
      `SKIP src/i18n/catalogs/en.json [schema-message-key] not yet created (i18n-strings-and-catalogs creates it)`.

## Anti-patterns

- **`onPress={form.handleSubmit(save)}`.** The promise is dropped and a throwing save becomes an
  unhandled rejection. Call `form.handleSubmit(save, onInvalid)().catch(record)` inside a void intent.
- **`v.checkAsync(isNameFree)` in the schema.** It runs on every keystroke after the first blur,
  never cancels and has no deadline. Use `useAsyncFieldCheck`.
- **`placeholder="Email"` as the only label.** It disappears while typing and screen readers do not
  treat it as the name. Show a label above the field.
- **`watch()` to show live values or derive submit state.** React Compiler cannot memoize it (lint
  `react-hooks/incompatible-library`) and it re-renders the whole form per keystroke. Use
  `useWatch({ name })` or `useFormState`.
- **`disabled` on the Save button until the form is valid.** The user cannot find out what is wrong.
  Keep Save pressable; a refused submit focuses and announces the first problem.
- **Wrapping the screen in React Native's `KeyboardAvoidingView` or a plain `ScrollView`.** Android
  edge-to-edge ignores the resize, and the focused field ends up under the keyboard.
- **`keyboardType="numeric"` plus `Number(text)`.** The decimal separator and the digits depend on
  the keyboard's language. Use the `integer` or `decimal` kind and `toLatinDigits` in the schema.
- **`form.reset()` right after calling the save, or inside `handleSubmit`'s callback.** A failed
  write throws the input away; a reset inside the callback leaves the form "submitted". Reset in
  the `.then` after `handleSubmit(...)()`, only when the save returned `ok`.
- **Translating in the schema (`v.nonEmpty(t('...'))`).** The schema is built once, the language can
  change, and the checkers cannot see the key. Pass the key; the field translates.
- **`KeyboardToolbar` from react-native-keyboard-controller as the Done bar.** In 1.21.9 its buttons
  speak fixed English labels and cap their text at 130 %; check-forms does not count it unless the
  app lists it in `forms.doneBars`. Use `KeyboardDoneBar`.
- **A fixed `bottomOffset` next to a sticky bar.** The bar rides on the keyboard and covers the
  bottom of the focused field. Take the offset from `useDoneBarSpace`.

## Files in this skill

| File | What it is | Read/run when |
| --- | --- | --- |
| `references/schemas-and-messages.md` | valibot and the resolver, input and output types, message keys, digits, Segmenter, cross-field and date fields | writing a schema (step 6) |
| `references/fields-and-autofill.md` | TextField, the kinds, autofill on iOS and Android (verified mapping), labels and screen readers, RTL | adding a field or a kind (step 7) |
| `references/keyboard-focus-and-submit.md` | modes, focus chain, keyboard-controller, edge-to-edge, Done bar, submit, drafts, leaving a dirty form | wiring the model and screen (steps 4, 7, 9) |
| `references/async-checks-and-server-errors.md` | useAsyncFieldCheck design, races, timing, server refusals to fields | an async or server check (step 8) |
| `references/checks-and-tests.md` | every check-forms rule with its fix, config keys, limits, test recipes | running the checker, writing tests (steps 1, 10, 11) |
| `templates/src/ui/form/` | `field-kinds.ts`, `text-field.tsx` (with an Android-preset test), `field-note.tsx`, `keyboard-done-bar.tsx`, with tests | step 3 |
| `templates/src/hooks/` | `use-text-field.ts`, `field-message-key.ts`, `use-async-field-check.ts`, `use-done-bar-space.ts`, with tests | step 3 |
| `templates/src/core/text/` | `latin-digits.ts`: digits of any script to ASCII, with its test | step 3 |
| `templates/src/features/task-editor/` | the New task form: schema, model, screen, tests (the pattern to copy) | step 6 |
| `templates/src/i18n/catalogs/` | `en.json`, `ar.json`: the `forms.*` and `task-editor.*` keys to merge | step 3 |
| `templates/test/forms/i18n-form-wrapper.tsx` | test providers with the real catalogs, failing on a missing key | step 10 |
| `templates/__mocks__/react-native-keyboard-controller.ts` | canonical root Jest mock (synced, owned here) | step 3 |
| `examples/root-stack-keyboard-provider.tsx` | where KeyboardProvider goes in the root stack | step 4 |
| `examples/list-name-availability.ts` | an async availability check wired to a field and to submit | step 8 |
| `examples/server-field-errors.ts` | server `rejected` codes mapped to field keys with `setError` | step 8 |
| `examples/account-fields.ts` | name, email and new-password kinds chained for autofill | step 7 |
| `assets/component-library-entries.json` | the field components' entries for the component catalogue | step 5 |
| `assets/shared.json` | the files synced from the library's canonical copies | maintainers only |
| `scripts/check-forms.mjs` | the checker (17 rules) | steps 1 and 11 |
| `scripts/selftest.mjs` | proves the checker against `tests/fixtures/` | after changing the checker |
| `scripts/check-lib.mjs` | shared checker library (synced; do not edit) | never by hand |
| `scripts/lib/` | `source-scan.mjs`, the shared source lexer and JSX helpers (synced; do not edit) | never by hand |
| `tests/fixtures/` | planted-bug cases (one `bad-*` per rule), `pass-*`, `error-*` and `good/` | after changing the checker |
| `tests/bases/app/` | the small Tasks base the fixtures start from (with the templates) | after changing the checker |

## Related skills

- `ui-component-library` - the component catalogue the field components join; buttons and rows.
- `i18n-strings-and-catalogs` - catalogs, keys and translation of every label and message.
- `accessibility-as-code` - touch targets, contrast, Dynamic Type and screen-reader rules.
- `error-handling-typed-results` - `Result`, failures and never losing data on an error path.
- `async-safety` - the latest-request, delay and deadline helpers the async check builds on.
- `networking-and-server-state` - the `ApiClient`, mutations and `rejected` failures.
- `auth-and-secure-storage` - sign-in flows, passwords and tokens.
- `dates-streaks-and-statistics` - date fields holding a calendar day.
- `value-objects-money-and-units` - `normalizeDecimalInput`, money and quantity parsing for amount fields.
- `rtl-and-direction` - mixed-script text and logical alignment.
- `navigation-and-routing` - leaving a form with unsaved changes; the root stack order.
- `unit-and-component-tests` - render helpers, mocks and Jest setup.

Files in this skill

  • SKILL.md18.6 KB
  • assets/component-library-entries.json652 B
  • assets/shared.json298 B
  • examples/account-fields.ts1.9 KB
  • examples/list-name-availability.ts4.9 KB
  • examples/root-stack-keyboard-provider.tsx1.4 KB
  • examples/server-field-errors.ts1.6 KB
  • references/async-checks-and-server-errors.md8.8 KB
  • references/checks-and-tests.md14.7 KB
  • references/fields-and-autofill.md15 KB
  • references/keyboard-focus-and-submit.md16.4 KB
  • references/schemas-and-messages.md16.4 KB
  • scripts/check-forms.mjs39.6 KB
  • scripts/lib/source-scan.mjs35.1 KB
  • scripts/selftest.mjs392 B
  • templates/__mocks__/react-native-keyboard-controller.ts459 B
  • templates/src/core/text/latin-digits.test.ts740 B
  • templates/src/core/text/latin-digits.ts1.6 KB
  • templates/src/features/task-editor/task-editor-schema.test.ts3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.