react-formbridge
Browse documentation
Stylingv1.0.2

globalDefaults

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

InlineExample-2.tsts
1useFormBridge(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 from globalDefaults.field. The merge order is: builder behaviorglobalDefaults → local field props → fieldController / custom render.
  • Platform-aware typing. On web, generated fields expose slot maps such as classNames / styles, plus DOM passthrough props like wrapperProps and inputProps. On native, fields expose RN-friendly styles, wrapperProps, keyboardType, secureTextEntry, and submit-specific props such as containerStyle, textStyle, and indicatorColor. The hook variant you import (useFormBridge web 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().

ReactiveTheme.web.tsxtsx
1import { field, useFormBridge } from '@runilib/react-formbridge'
2import styles from './Form.module.css'
3
4const schema = {
5 email: field.email('Email').required(),
6 password: field.password('Password').required().min(8),
7}
8
9export 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:

InlineExample-1.tsts
1globalDefaults?(state: FormState<S>): FormBridgeOptions<TPlatform>

Fields available on state (non-exhaustive - see the useFormBridge() section for the full list):

FieldDescription
valuesCurrent values, typed from the schema
errorsPer-field error map
formLevelErrorForm-level error string produced by createSchema() refinements (null when none)
touched / dirtyPer-field tracking bags
isValid / isDirty / isSubmitting / isSubmitted / submitCountForm-level flags
submitErrorString set by onSubmitError(error) when your onSubmit throws

Because the selector receives state, the theme can react:

  • Switch submit.loadingText while state.isSubmitting is true
  • Add a formLevelError className when state.submitError is 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:

  1. Builder behavior - anything declared on the schema builder itself (e.g. field.text().placeholder('…').hint('…')). Lowest precedence.
  2. globalDefaults: the function documented here. Covers every field, the form wrapper, and the submit button.
  3. Local field props - anything passed directly on <fields.name classNames={...} /> or on a <Form ...> / <Form.Submit ...> call site. Wins over global config.
  4. 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 repeat classNames / styles on 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

KeyDescription
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

KeyDescription
classNames?Per-slot class names for renderers such as wrapper, label, textInput, textarea, or select

Native-only extras

KeyDescription
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:

KeyWhy it must stay local
autoCompletePer-field autofill hint
autoFocusOnly one field should take focus on mount
enterKeyHintPer-field "enter key" label on mobile
spellCheckPer-field toggle
idMust be unique per field

Declare those directly on the specific <fields.*> call site.

global-field.tsxtsx
1useFormBridge(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

KeyTypeDescription
className?stringClass added to the <form> element
style?CSSPropertiesInline style merged onto the <form> element
props?HTMLFormAttributesPassthrough attributes spread on <form> (minus FormBridge-owned: children, onSubmit, className, style)

Native

KeyTypeDescription
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.

global-form.tsxtsx
1useFormBridge(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

KeyTypeDescription
className?stringClass on the <button>
style?CSSPropertiesInline style on the <button>
loadingText?ReactNodeContent shown while submitting (defaults to "Please wait…")
props?HTMLButtonAttributesPassthrough attributes spread on <button> (minus FormBridge-owned: children, type, disabled, className, style)

Native

KeyTypeDescription
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?stringColor of the ActivityIndicator shown while submitting
loadingText?ReactNodeContent 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.

global-submit.tsxtsx
1useFormBridge(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:

MethodTypeDescription
globalDefaults(state) => FormBridgeUiOptionsFunction 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.

FieldSlot names
Shared (every field)wrapper, label, hint, error, requiredMark
Text / email / number / tel / url / datetextInput
Textareatextarea
Selectselect, selectValue, selectArrow
CheckboxcheckboxRow, checkboxInput, checkboxLabel
RadioradioGroup, radioOption, radioInput, radioLabel
SwitchswitchRoot, switchButton, switchTrack, switchThumb, switchLabel
OTPotpContainer, otpInput, otpSeparator
PasswordpasswordInput, passwordToggle, passwordStrengthRow, passwordStrengthBar, passwordStrengthMeta, passwordStrengthFill, passwordStrengthLabel, passwordStrengthEntropy, passwordRulesList, passwordRuleItem, passwordRuleBullet, passwordRuleText
PhonephoneInput, phoneRow, phoneCountryButton, phoneCountryFlag, phoneCountryDivider, phoneChevron, phoneSearchInput, phoneSearchWrapper, phoneCountryList, phoneCountryScroll, phoneCountryItem, phoneSeparator, phoneCountryName, phoneCountryDial, phoneE164, phoneEmptyText
FilefileDropZone, fileDropZoneIcon, fileDropZoneText, fileDropZoneAccept, fileDropZoneMaxSize, fileBrowseButton, fileList, fileListItem, filePreviewImage, fileIcon, fileInfo, fileName, fileMeta, fileRemoveButton, fileAddMoreButton
Async autocompleteautocompleteInput, 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.

FieldSlot names
Shared (every field)wrapper, label, error, hint, requiredMark
Text / email / number / tel / url / datetextInput
CheckboxcheckboxRow, checkboxBox, checkboxLabel
SwitchswitchRow, switchLabel
Select / radioselectTrigger, selectTriggerLabel, selectOptionRow, selectOptionLabel, selectModalBackdrop, selectModalCard
OTPotpContainer, otpInput, otpSeparator
PasswordpasswordInput, passwordToggle, passwordToggleText, passwordStrengthRow, passwordStrengthBar, passwordStrengthMeta, passwordStrengthFill, passwordStrengthLabel, passwordStrengthEntropy, passwordRulesList, passwordRuleItem, passwordRuleBullet, passwordRuleText
PhonephoneInput, phoneRow, phoneCountryButton, phoneCountryFlag, phoneCountryDial, phoneCountryDivider, phoneChevron, phoneE164, phoneModalBackdrop, phoneModalCard, phoneSearchInput, phoneSeparator, phoneCountryRow, phoneCountryName, phoneEmptyText
FilefilePickButton, filePickButtonText, fileList, fileItem, fileIcon, fileIconText, fileName, fileMeta, fileRemoveButton, fileRemoveText
Async autocompleteautocompleteTrigger, 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.