Disabled and Read-Only Components
Many component libraries expose disabled and readOnly flags. disabled usually blocks interaction, while readOnly allows viewing
without editing. FormEngine Core supports these flags out of the box and propagates them through the component tree so you can lock down
sections consistently without wiring every field.
In this guide, you'll learn:
- How
disabledandreadOnlyare computed and propagated - Where to set the flags in
FormViewerand form JSON - Which components receive the flags and why
- How UI components apply the flags
Overview
disabled and readOnly are boolean flags that move down the component tree. FormEngine Core treats both as propagated state and leaves
the actual interaction behavior to the UI component, so you can define intent once and let each component decide how to react.
Where to Set the Flags
Set the flags at the highest level that matches your intent. Because the flags always propagate down, a higher-level setting affects every component beneath it.
| Source | How to set | Scope |
|---|---|---|
| FormViewer props | readOnly disabled | Entire component tree |
| Screen props | readOnly disabled | Screen subtree |
| Templates / embedded forms | readOnly disabled | Template subtree |
| Container components | readOnly or disabled propsSee the API reference. | Container subtree |
| Component props | readOnly or disabled propsSee the API reference. | Component subtree |
- FormViewer props apply across the tree and are part of the public API. See FormViewerProps.
- Container components are any components that render children. If they declare these annotations, the flags apply to all descendants. Not every container exposes these flags, so check the component API reference.
- Component props are the most direct way to set flags, but not every component supports them. Check the component API reference.
- Custom prop mapping is supported. If a component maps the flag to a custom key, set that key in form JSON (for example,
isLocked).
FormViewer props examples
Read-only
- Define a form with a single
MuiTextField. - Pass
readOnlytoFormViewer.
Live example
function App() { const form = { form: { key: 'Screen', type: 'Screen', children: [ { key: 'name', type: 'MuiTextField', props: { label: { value: 'Name', }, value: { value: 'FormEngine' } }, }, ], }, } const getForm = useCallback(() => JSON.stringify(form), [form]) return ( <FormViewer view={muiView} getForm={getForm} readOnly /> ) }
Disabled
- Define a form with a single
MuiTextField. - Pass
disabledtoFormViewer.
Live example
function App() { const form = { form: { key: 'Screen', type: 'Screen', children: [ { key: 'name', type: 'MuiTextField', props: { label: { value: 'Name', }, value: { value: 'FormEngine' } }, }, ], }, } const getForm = useCallback(() => JSON.stringify(form), [form]) return ( <FormViewer view={muiView} getForm={getForm} disabled /> ) }
Screen props examples
Read-only
- Set
readOnlyon theScreenprops in the form JSON. - Render the form with
FormViewer.
Live example
function App() { const form = { form: { key: 'Screen', type: 'Screen', props: { readOnly: { value: true, }, }, children: [ { key: 'name', type: 'MuiTextField', props: { label: { value: 'Name', }, value: { value: 'FormEngine' } }, }, ], }, } const getForm = useCallback(() => JSON.stringify(form), [form]) return ( <FormViewer view={muiView} getForm={getForm} /> ) }
Disabled
- Set
disabledon theScreenprops in the form JSON. - Render the form with
FormViewer.
Live example
function App() { const form = { form: { key: 'Screen', type: 'Screen', props: { disabled: { value: true, }, }, children: [ { key: 'name', type: 'MuiTextField', props: { label: { value: 'Name', }, value: { value: 'FormEngine' } }, }, ], }, } const getForm = useCallback(() => JSON.stringify(form), [form]) return ( <FormViewer view={muiView} getForm={getForm} /> ) }
Embedded forms example
- Define a single embedded form with one
MuiTextField. - In the parent form, render two
EmbeddedFormcomponents that share the sameformName. - Add labels in the parent form and set
readOnlyon one embedded form anddisabledon the other. - Use
getFormto return the embedded form whenformNameis provided.
Live example
function App() { const embeddedForm = { form: { key: 'Screen', type: 'Screen', children: [ { key: 'name', type: 'MuiTextField', props: { label: { value: 'Embedded name', }, }, }, ], }, } const form = { form: { key: 'Screen', type: 'Screen', children: [ { key: 'readOnlyLabel', type: 'MuiTypography', props: { children: { value: 'Read-only embedded form', }, }, }, { key: 'readOnlyEmbedded', type: 'EmbeddedForm', props: { formName: { value: 'profile', }, readOnly: { value: true, }, }, }, { key: 'disabledLabel', type: 'MuiTypography', props: { children: { value: 'Disabled embedded form', }, }, }, { key: 'disabledEmbedded', type: 'EmbeddedForm', props: { formName: { value: 'profile', }, disabled: { value: true, }, }, }, ], }, } const getForm = useCallback((name) => { if (!name) return JSON.stringify(form) if (name === 'profile') return JSON.stringify(embeddedForm) return JSON.stringify({form: {key: 'Screen', type: 'Screen', children: []}}) }, [form, embeddedForm]) return ( <FormViewer view={muiView} getForm={getForm} /> ) }
Container components example
- Wrap each
MuiTextFieldin its ownMuiBoxcontainer and add a label withMuiTypography. - Set
readOnlyon one container anddisabledon the other. - Render the form with
FormViewer.
Live example
function App() { const form = { form: { key: 'Screen', type: 'Screen', children: [ { key: 'readOnlyContainer', type: 'MuiBox', props: { readOnly: { value: true, }, }, children: [ { key: 'readOnlyLabel', type: 'MuiTypography', props: { children: { value: 'Read-only container', }, }, }, { key: 'readOnlyInput', type: 'MuiTextField', props: { label: { value: 'Name', }, }, }, ], }, { key: 'disabledContainer', type: 'MuiBox', props: { disabled: { value: true, }, }, children: [ { key: 'disabledLabel', type: 'MuiTypography', props: { children: { value: 'Disabled container', }, }, }, { key: 'disabledInput', type: 'MuiTextField', props: { label: { value: 'Name', }, }, }, ], }, ], }, } const getForm = useCallback(() => JSON.stringify(form), [form]) return ( <FormViewer view={muiView} getForm={getForm} /> ) }
Component props example
- Define two
MuiTextFieldcomponents in the form. - Set
readOnlyon one component anddisabledon the other. - Render the form with
FormViewer.
Live example
function App() { const form = { form: { key: 'Screen', type: 'Screen', children: [ { key: 'readOnlyInput', type: 'MuiTextField', props: { label: { value: 'Read-only name', }, readOnly: { value: true, }, }, }, { key: 'disabledInput', type: 'MuiTextField', props: { label: { value: 'Disabled name', }, disabled: { value: true, }, }, }, ], }, } const getForm = useCallback(() => JSON.stringify(form), [form]) return ( <FormViewer view={muiView} getForm={getForm} /> ) }
Propagation Rules
For each component, the core computes isDisabled and isReadOnly from three sources:
FormViewerprops- The parent component state
- The component's own mapped flag
const isDisabled = viewer.disabled || parent.isDisabled || self.disabled
const isReadOnly = viewer.readOnly || parent.isReadOnly || self.readOnly
self refers to the component's own flag (or mapped prop key). Propagation uses the parent component state, not raw parent props, so
descendants inherit the already computed flag. A true value at any level applies to the entire subtree. There is no override to turn a
child back on if an ancestor is disabled or read-only.
Which Components Receive the Flags
Only components that declare disabled or readOnly in their metadata receive the computed flags as props. If a component does not opt in,
the core does not inject these flags for it.
Use the special boolean annotations when defining your component metadata:
- Declare the flag in
.props()using thereadOnlyordisabledannotation. - Map to your prop name by choosing the key that matches your component API (for example,
isLockedorisDisabled). - Remember containers: container components can also declare these annotations to propagate the flags to their children.
Short examples:
export const simpleField = define(SimpleField, 'SimpleField')
.props({
readOnly: readOnly,
disabled: disabled,
})
.build()
export const customField = define(CustomField, 'CustomField')
.props({
isLocked: readOnly,
isDisabled: disabled,
})
.build()
Example: Simple Read-Only Component
import type {ChangeEvent} from 'react'
import {define, disabled, readOnly, string} from '@react-form-builder/core'
interface SimpleNoteProps {
value: string
isLocked?: boolean
isDisabled?: boolean
onChange?: (value: string) => void
}
const baseStyle = {
width: '100%',
padding: '8px 12px',
borderRadius: 6,
border: '1px solid #d1d5db',
fontSize: 14,
lineHeight: 1.4,
} as const
const getInputStyle = (isLocked?: boolean, isDisabled?: boolean) => ({
...baseStyle,
backgroundColor: isDisabled ? '#f3f4f6' : isLocked ? '#f9fafb' : '#ffffff',
color: isDisabled ? '#9ca3af' : isLocked ? '#6b7280' : '#111827',
})
const SimpleNote = ({value, isLocked, isDisabled, onChange}: SimpleNoteProps): JSX.Element => (
<input
value={value}
readOnly={isLocked}
disabled={isDisabled}
onChange={(event: ChangeEvent<HTMLInputElement>) => onChange?.(event.target.value)}
style={getInputStyle(isLocked, isDisabled)}
/>
)
export const simpleNote = define(SimpleNote, 'SimpleNote')
.props({
value: string.valued.uncontrolledValue(''),
isLocked: readOnly,
isDisabled: disabled,
})
.build()
Live example
function App() { const baseStyle = { width: '100%', padding: '8px 12px', borderRadius: 6, border: '1px solid #d1d5db', fontSize: 14, lineHeight: 1.4, } const getInputStyle = (isLocked, isDisabled) => ({ ...baseStyle, backgroundColor: isDisabled ? '#f3f4f6' : isLocked ? '#f9fafb' : '#ffffff', color: isDisabled ? '#9ca3af' : isLocked ? '#6b7280' : '#111827', }) const SimpleNote = ({value, isLocked, isDisabled, onChange}) => ( <input value={value} readOnly={isLocked} disabled={isDisabled} onChange={(event) => onChange?.(event.target.value)} style={getInputStyle(isLocked, isDisabled)} /> ) const simpleNote = define(SimpleNote, 'SimpleNote') .props({ value: string.valued.uncontrolledValue(''), isLocked: readOnly, isDisabled: disabled, }) .build() const view = muiView view.define(simpleNote.model) const form = { form: { key: 'Screen', type: 'Screen', children: [ { key: 'defaultContainer', type: 'MuiBox', children: [ { key: 'defaultNote', type: 'SimpleNote', props: { value: { value: 'Editable', }, }, }, ], }, { key: 'readOnlyContainer', type: 'MuiBox', props: { readOnly: { value: true, }, }, children: [ { key: 'readOnlyNote', type: 'SimpleNote', props: { value: { value: 'Read-only', }, }, }, ], }, { key: 'disabledContainer', type: 'MuiBox', props: { disabled: { value: true, }, }, children: [ { key: 'disabledNote', type: 'SimpleNote', props: { value: { value: 'Disabled', }, }, }, ], }, ], }, } const getForm = useCallback(() => JSON.stringify(form), [form]) return <FormViewer view={view} getForm={getForm}/> }
How UI Components Apply the Flags
FormEngine Core injects disabled and readOnly alongside calculated values and events, but it does not block interaction on its own. Each
UI component must apply the flags, for example by mapping to native props when supported or guarding event handlers like onChange and
onClick.
Form actions and calculations can still read and update form data even when components are disabled or read-only. The flags only influence how the UI behaves.
Summary
disabledandreadOnlyare computed fromFormViewerprops, parent state, and the component's own mapped flag.- Only components that declare these annotations receive the flags as props.
- The core injects the flags into rendered props, while UI components decide how to enforce them.
- You can set the flags on
FormViewer, screens, embedded forms/templates, containers, or individual components.
For more information: