globalDefaults is the single place where you theme every generated field, the `<Form>` wrapper, and `Form.Submit` at once. You pass it to useFormBridge(schema, { globalDefaults }) and it becomes the shared visual layer for the form - CSS Modules, StyleSheet, utility classes, or design-system components all plug in here.
Signature
| 1 | useFormBridge(schema, { |
| 2 | globalDefaults: (state) => ({ |
| 3 | field?: FieldTheme, // applied to every rendered field |
| 4 | form?: FormTheme, // applied to the <Form> wrapper |
| 5 | submit?: SubmitTheme, // applied to Form.Submit |
| 6 | }), |
| 7 | }) |
- It's a function, not a static object. You receive the live
FormState<S>(isSubmitting,isValid,isDirty,errors,values,submitError, …) and return an options bag. This means the theme can react to form state: highlight the form red on submit error, change the submit label while submitting, dim fields while the form is busy, etc.
- Local field props still win. Anything you pass directly on
<fields.email classNames={...} />overrides the matching key fromglobalDefaults.field. The merge order is: builderbehavior→globalDefaults→ local field props →fieldController/ custom render.
- Platform-aware typing. On web, generated fields expose slot maps such as
classNames/styles, plus DOM passthrough props likewrapperPropsandinputProps. On native, fields expose RN-friendlystyles,wrapperProps,keyboardType,secureTextEntry, and submit-specific props such ascontainerStyle,textStyle, andindicatorColor. The hook variant you import (useFormBridgeweb vs native) selects the correct shape automatically.
Prefer globalDefaults over per-field overrides as soon as two or more fields need the same look. Reach for local field props only for genuine one-off exceptions. For full custom chrome beyond styling, see fieldController or field.custom().
| 1 | import { field, useFormBridge } from '@runilib/react-formbridge' |
| 2 | import styles from './Form.module.css' |
| 3 | |
| 4 | const schema = { |
| 5 | email: field.email('Email').required(), |
| 6 | password: field.password('Password').required().min(8), |
| 7 | } |
| 8 | |
| 9 | export function LoginForm() { |
| 10 | const { Form, fields, state } = useFormBridge(schema, { |
| 11 | validateOn: 'onTouched', |
| 12 | globalDefaults: (state) => ({ |
| 13 | form: { |
| 14 | className: `${styles.form} ${state.submitError ? styles.formLevelError : ''}`, |
| 15 | }, |
| 16 | field: { |
| 17 | classNames: { |
| 18 | wrapper: styles.fieldWrapper, |
| 19 | label: styles.label, |
| 20 | textInput: styles.input, |
| 21 | error: styles.error, |
| 22 | hint: styles.hint, |
| 23 | requiredMark: styles.required, |
| 24 | }, |
| 25 | highlightOnError: true, |
| 26 | }, |
| 27 | submit: { |
| 28 | className: styles.submit, |
| 29 | loadingText: state.isSubmitting ? 'Signing in…' : 'Sign in', |
| 30 | }, |
| 31 | }), |
| 32 | }) |
| 33 | |
| 34 | return ( |
| 35 | <Form onSubmit={(v) => api.login(v)}> |
| 36 | <fields.email /> |
| 37 | <fields.password /> |
| 38 | <Form.Submit>Sign in</Form.Submit> |
| 39 | </Form> |
| 40 | ) |
| 41 | } |
Signature & reactive state
globalDefaults runs on every render of the form, with the latest FormState<S> as its only argument:
| 1 | globalDefaults?(state: FormState<S>): FormBridgeOptions<TPlatform> |
Fields available on state (non-exhaustive - see the useFormBridge() section for the full list):
| Field | Description |
|---|---|
values | Current values, typed from the schema |
errors | Per-field error map |
formLevelError | Form-level error string produced by createSchema() refinements (null when none) |
touched / dirty | Per-field tracking bags |
isValid / isDirty / isSubmitting / isSubmitted / submitCount | Form-level flags |
submitError | String set by onSubmitError(error) when your onSubmit throws |
Because the selector receives state, the theme can react:
- Switch
submit.loadingTextwhilestate.isSubmittingistrue - Add a
formLevelErrorclassName whenstate.submitErroris set - Swap the submit button className or style when the form becomes valid / dirty
- Tint every field wrapper when the form has unresolved errors
Return the same shape regardless of state - React just re-renders the theme each time.
Merge order & precedence
FormBridge merges style/behavior from four layers, always in the same order:
- Builder behavior - anything declared on the schema builder itself (e.g.
field.text().placeholder('…').hint('…')). Lowest precedence. - globalDefaults: the function documented here. Covers every field, the form wrapper, and the submit button.
- Local field props - anything passed directly on
<fields.name classNames={...} />or on a<Form ...>/<Form.Submit ...>call site. Wins over global config. - fieldController / field.custom().render(...) - fully custom render layer. Wins over everything above because at that point FormBridge is no longer rendering the chrome itself.
Practical consequences:
- Change the whole form's look once in
globalDefaults- no need to repeatclassNames/styleson every<fields.*>call site. - Override a single field locally with
<fields.email classNames={{ wrapper: 'narrow' }} />without touching the global theme. - Keep one-off exceptions local; keep shared language global. That's the mental model.
field: shared defaults for every rendered field
Everything under globalDefaults.field is forwarded to every <fields.*> component unless a local prop overrides it. The shape is FieldTheme<PlatformGlobalFieldPropsOverrides<TPlatform>> - identical to the per-field override type minus a few props that must stay local (see the caveat below).
Available on both web and native
| Key | Description |
|---|---|
styles? | Per-slot style overrides (web + native) |
hideLabel? | Hide visual labels while keeping them for screen readers |
highlightOnError? | Turn the default red error chrome on/off |
readOnly? | Mark every field read-only (handy for "view mode") |
wrapperProps? / labelProps? / hintProps? / errorProps? | Passthrough props for the DOM nodes of each slot |
inputProps / textareaProps / selectProps / buttonProps / … | Per-type passthrough - FormBridge routes them to the matching renderer |
Web-only extras
| Key | Description |
|---|---|
classNames? | Per-slot class names for renderers such as wrapper, label, textInput, textarea, or select |
Native-only extras
| Key | Description |
|---|---|
keyboardType? / secureTextEntry? | RN-specific input hints for text-like fields |
Caveat: props that must stay local, not global
These keys make sense only on an individual field and are omitted from the globalDefaults.field type to prevent accidents:
| Key | Why it must stay local |
|---|---|
autoComplete | Per-field autofill hint |
autoFocus | Only one field should take focus on mount |
enterKeyHint | Per-field "enter key" label on mobile |
spellCheck | Per-field toggle |
id | Must be unique per field |
Declare those directly on the specific <fields.*> call site.
| 1 | useFormBridge(schema, { |
| 2 | globalDefaults: () => ({ |
| 3 | field: { |
| 4 | classNames: { |
| 5 | wrapper: 'fb-field__wrapper', |
| 6 | label: 'fb-field__label', |
| 7 | textInput: 'fb-field__input', |
| 8 | textarea: 'fb-field__input', |
| 9 | select: 'fb-field__input', |
| 10 | error: 'fb-field__error', |
| 11 | hint: 'fb-field__hint', |
| 12 | requiredMark: 'fb-field__required', |
| 13 | }, |
| 14 | hideLabel: false, |
| 15 | highlightOnError: true, |
| 16 | wrapperProps: { 'data-testid': 'field' }, |
| 17 | }, |
| 18 | }), |
| 19 | }) |
form: overrides applied to the <Form> wrapper
Attach styling and passthrough props to the <Form> wrapper element.
Web
| Key | Type | Description |
|---|---|---|
className? | string | Class added to the <form> element |
style? | CSSProperties | Inline style merged onto the <form> element |
props? | HTMLFormAttributes | Passthrough attributes spread on <form> (minus FormBridge-owned: children, onSubmit, className, style) |
Native
| Key | Type | Description |
|---|---|---|
style? | StyleProp<ViewStyle> | Style applied to the wrapper <View> |
props? | Record<string, unknown> | Passthrough props spread on the wrapper <View> |
Event handlers like onSubmit, onError, and onSubmitError are set on the <Form> call site, not here - globalDefaults is for visual/theming concerns.
| 1 | useFormBridge(schema, { |
| 2 | globalDefaults: (state) => ({ |
| 3 | form: { |
| 4 | className: state.submitError ? 'fb-form fb-form--error' : 'fb-form', |
| 5 | style: { display: 'grid', gap: 16 }, |
| 6 | props: { 'data-testid': 'checkout-form', autoComplete: 'on' }, |
| 7 | }, |
| 8 | }), |
| 9 | }) |
submit: overrides applied to Form.Submit
Style the submit button and drive its loading copy from form state.
Web
| Key | Type | Description |
|---|---|---|
className? | string | Class on the <button> |
style? | CSSProperties | Inline style on the <button> |
loadingText? | ReactNode | Content shown while submitting (defaults to "Please wait…") |
props? | HTMLButtonAttributes | Passthrough attributes spread on <button> (minus FormBridge-owned: children, type, disabled, className, style) |
Native
| Key | Type | Description |
|---|---|---|
style? | StyleProp<ViewStyle> | Style applied to the outer TouchableOpacity |
containerStyle? | StyleProp<ViewStyle> | Style applied to the inner content <View> |
textStyle? | StyleProp<TextStyle> | Style applied to the label <Text> |
indicatorColor? | string | Color of the ActivityIndicator shown while submitting |
loadingText? | ReactNode | Content shown next to the spinner while submitting |
props? | Record<string, unknown> | Passthrough props spread on the outer TouchableOpacity |
contentProps? | Record<string, unknown> | Passthrough props spread on the inner content <View> |
FormBridge manages disabled and the loading transition itself, so state.isSubmitting is the signal you use to drive loadingText / indicatorColor - you never flip disabled manually mid-submit.
| 1 | useFormBridge(schema, { |
| 2 | globalDefaults: (state) => ({ |
| 3 | submit: { |
| 4 | className: 'fb-submit', |
| 5 | loadingText: state.isSubmitting ? 'Saving…' : undefined, |
| 6 | }, |
| 7 | }), |
| 8 | }) |
Complete surface reference
Complete globalDefaults surface:
| Method | Type | Description |
|---|---|---|
globalDefaults | (state) => FormBridgeUiOptions | Function invoked on each render - can react to submit/dirty/error state |
field? | { classNames?, styles?, hideLabel?, highlightOnError?, readOnly?, wrapperProps?, ... } | Shared defaults for all generated fields |
form? (web) | { className?, style?, props? } | Form wrapper overrides on web |
form? (native) | { style?, props? } | Form wrapper overrides on native |
submit? (web) | { className?, style?, loadingText?, props? } | Submit button overrides on web |
submit? (native) | { style?, containerStyle?, textStyle?, indicatorColor?, loadingText?, props?, contentProps? } | Submit button overrides on native |
Web slot names
Every web field exposes a set of named slots so globalDefaults.field.classNames / styles can target each piece of the rendered field without reaching into the DOM.
Web field slot names currently exposed through classNames / styles.
Slot names are prefixed with the field type so it is clear where each override will land.
| Field | Slot names |
|---|---|
| Shared (every field) | wrapper, label, hint, error, requiredMark |
| Text / email / number / tel / url / date | textInput |
| Textarea | textarea |
| Select | select, selectValue, selectArrow |
| Checkbox | checkboxRow, checkboxInput, checkboxLabel |
| Radio | radioGroup, radioOption, radioInput, radioLabel |
| Switch | switchRoot, switchButton, switchTrack, switchThumb, switchLabel |
| OTP | otpContainer, otpInput, otpSeparator |
| Password | passwordInput, passwordToggle, passwordStrengthRow, passwordStrengthBar, passwordStrengthMeta, passwordStrengthFill, passwordStrengthLabel, passwordStrengthEntropy, passwordRulesList, passwordRuleItem, passwordRuleBullet, passwordRuleText |
| Phone | phoneInput, phoneRow, phoneCountryButton, phoneCountryFlag, phoneCountryDivider, phoneChevron, phoneSearchInput, phoneSearchWrapper, phoneCountryList, phoneCountryScroll, phoneCountryItem, phoneSeparator, phoneCountryName, phoneCountryDial, phoneE164, phoneEmptyText |
| File | fileDropZone, fileDropZoneIcon, fileDropZoneText, fileDropZoneAccept, fileDropZoneMaxSize, fileBrowseButton, fileList, fileListItem, filePreviewImage, fileIcon, fileInfo, fileName, fileMeta, fileRemoveButton, fileAddMoreButton |
| Async autocomplete | autocompleteInput, autocompleteSelect, autocompleteSelectValue, autocompleteSelectArrow, autocompleteListbox, autocompleteOption, autocompleteOptionActive, autocompleteOptionSelected, autocompleteEmpty, autocompleteLoading |
Native slot names
Every native field exposes a set of named style slots so globalDefaults.field.styles can target each piece of the rendered field.
Native field slot names currently exposed through styles.
Slot names are prefixed with the field type so it is clear where each override will land.
| Field | Slot names |
|---|---|
| Shared (every field) | wrapper, label, error, hint, requiredMark |
| Text / email / number / tel / url / date | textInput |
| Checkbox | checkboxRow, checkboxBox, checkboxLabel |
| Switch | switchRow, switchLabel |
| Select / radio | selectTrigger, selectTriggerLabel, selectOptionRow, selectOptionLabel, selectModalBackdrop, selectModalCard |
| OTP | otpContainer, otpInput, otpSeparator |
| Password | passwordInput, passwordToggle, passwordToggleText, passwordStrengthRow, passwordStrengthBar, passwordStrengthMeta, passwordStrengthFill, passwordStrengthLabel, passwordStrengthEntropy, passwordRulesList, passwordRuleItem, passwordRuleBullet, passwordRuleText |
| Phone | phoneInput, phoneRow, phoneCountryButton, phoneCountryFlag, phoneCountryDial, phoneCountryDivider, phoneChevron, phoneE164, phoneModalBackdrop, phoneModalCard, phoneSearchInput, phoneSeparator, phoneCountryRow, phoneCountryName, phoneEmptyText |
| File | filePickButton, filePickButtonText, fileList, fileItem, fileIcon, fileIconText, fileName, fileMeta, fileRemoveButton, fileRemoveText |
| Async autocomplete | autocompleteTrigger, autocompleteTriggerValue, autocompleteTriggerPlaceholder, autocompleteModalBackdrop, autocompleteModalCard, autocompleteSearchInput, autocompleteLoadingRow, autocompleteLoadingText, autocompleteOptionRow, autocompleteOptionLabel, autocompleteEmptyText |
When to use globalDefaults vs. alternatives
FormBridge offers three styling layers. Pick the right one for the scope of your change:
- globalDefaults - use when two or more fields need the same look, or when you want the theme to react to form state. Default recommendation for CSS Modules / StyleSheet / design-system-wide chrome. Declared once on
useFormBridge. - Local field props - use for one-off exceptions on a single field. Example:
<fields.email classNames={{ wrapper: 'narrow' }} />. Wins over global config. - fieldController / field.custom().render(...) - use when styling isn't enough and you need custom chrome (e.g. a bespoke phone picker UI on top of the built-in value model, or a totally new field type). See fieldController and field.custom().
The Styling section shows end-to-end examples that combine all three layers.