Advanced Features
Features for building production-quality form UIs: input adorners, readonly state, presentation variants, and options visibility.
Input Adorners (Prefix/Suffix)
Adorners are visual text decorators rendered inside the input boundary — for example a $ prefix on a currency field or a kg suffix on a weight field. They are visual-only and do not modify the stored value.
Supported Field Types
Adorners are available on adornable field types: text, email, url, password, textarea, number, and integer.
Props
| Property | Type | Description |
|---|---|---|
prefix | string | undefined | Text displayed before the input |
suffix | string | undefined | Text displayed after the input |
These properties are on field alongside other common props. They are only populated when the Forma spec defines prefix or suffix on the field.
Implementation
Wrap the input element with prefix/suffix content when present:
import type { TextComponentProps } from "@fogpipe/forma-react";
function TextInput({ field }: TextComponentProps) {
const hasAdorner = field.prefix || field.suffix;
const input = (
<input
id={field.name}
type="text"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
onBlur={field.onBlur}
placeholder={field.placeholder}
disabled={!field.enabled}
/>
);
if (!hasAdorner) return input;
return (
<div className="input-group">
{field.prefix && <span className="input-addon">{field.prefix}</span>}
{input}
{field.suffix && <span className="input-addon">{field.suffix}</span>}
</div>
);
}
Forma Spec Example
{
"id": "price",
"type": "number",
"label": "Price",
"prefix": "$",
"suffix": "USD"
}
Readonly vs Disabled
Fields support two distinct non-editable states:
| State | Appearance | Editable | Value Submitted | Use Case |
|---|---|---|---|---|
readonly | Clear, normal text | No | Yes | Confirmed values, reference data |
disabled | Greyed out | No | Yes | Dependent fields not yet applicable |
Props
field.readonly—booleanindicating the field is readonlyfield.enabled—booleanindicating the field is enabled (inverse of disabled)
Implementation
import type { TextComponentProps } from "@fogpipe/forma-react";
function TextInput({ field }: TextComponentProps) {
return (
<input
id={field.name}
type="text"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
onBlur={field.onBlur}
disabled={!field.enabled}
readOnly={field.readonly}
className={field.readonly ? "cursor-default focus:ring-0" : ""}
/>
);
}
The readOnly HTML attribute allows the field to remain focusable and its value to be submitted, while preventing edits. Style readonly fields to look normal but non-interactive (e.g., no focus ring, default cursor).
Presentation Variants
The variant and variantConfig properties let a single field type render as different UI components. For example, a number field can appear as a default input, a slider, a stepper, or an NPS scale — all without changing the field type.
Props
| Property | Type | Description |
|---|---|---|
variant | string | undefined | Variant name |
variantConfig | Record<string, unknown> | undefined | Variant-specific configuration |
These are available on all field types via BaseFieldProps.
Available Variants
| Field Type | Variant | Description | variantConfig Keys |
|---|---|---|---|
number | slider | Range slider | showValue (boolean) |
number | stepper | Increment/decrement buttons | - |
number | nps | 0-10 NPS scale | lowLabel, highLabel |
number | rating-stars | Star rating | maxStars (default 5) |
number | rating-numeric | Numeric rating buttons | maxRating (default 5) |
select | radio | Radio button group | - |
select | radio-cards | Card-style radio options | - |
select | button-group | Segmented button group | - |
display | heading | Section heading | level (2-5) |
display | divider | Visual separator | - |
display | metric | Large numeric display | unit |
display | alert | Alert with severity | severity ("info", "warning", "error", "success") |
display | callout | Informational callout | icon (e.g., "info", "lightbulb") |
display | summary | Card-based summary | - |
Implementation Pattern
Check the variant and route to the appropriate sub-component, falling back to the default:
import type { NumberComponentProps } from "@fogpipe/forma-react";
function NumberField({ field, spec }: NumberComponentProps) {
switch (field.variant) {
case "slider":
return <SliderField field={field} spec={spec} />;
case "stepper":
return <StepperField field={field} spec={spec} />;
case "nps":
return <NPSScale field={field} spec={spec} />;
case "rating-stars":
return <StarRating field={field} spec={spec} />;
default:
return <NumberInput field={field} spec={spec} />;
}
}
Forma Spec Example
{
"id": "satisfaction",
"type": "number",
"label": "How likely are you to recommend us?",
"variant": "nps",
"variantConfig": {
"lowLabel": "Not at all likely",
"highLabel": "Extremely likely"
}
}
Options Visibility
Select and multi-select field options support conditional visibility through visibleWhen FEEL expressions. The library evaluates these expressions and pre-filters the options — your component receives only the visible options in field.options. No client-side filtering is needed.
How It Works
In the Forma spec, individual options can define visibleWhen:
{
"id": "position",
"type": "select",
"label": "Position",
"options": [
{
"value": "dev",
"label": "Developer",
"visibleWhen": "department = \"engineering\""
},
{
"value": "designer",
"label": "Designer",
"visibleWhen": "department = \"design\""
},
{ "value": "manager", "label": "Manager" }
]
}
When department is "engineering", field.options contains ["Developer", "Manager"]. When department is "design", it contains ["Designer", "Manager"]. Options without visibleWhen are always visible.
Implementation (Options Visibility)
Since options are pre-filtered, your select component works the same as always:
import type { SelectComponentProps } from "@fogpipe/forma-react";
function SelectField({ field }: SelectComponentProps) {
return (
<select
value={field.value ?? ""}
onChange={(e) => field.onChange(e.target.value || null)}
>
<option value="">Select...</option>
{field.options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}
Dependent Selects Pattern
Options visibility is commonly used for cascading/dependent selects where one field's value controls which options appear in another:
{
"fields": {
"department": {
"type": "select",
"label": "Department",
"options": [
{ "value": "engineering", "label": "Engineering" },
{ "value": "design", "label": "Design" }
]
},
"role": {
"type": "select",
"label": "Role",
"options": [
{
"value": "frontend",
"label": "Frontend Dev",
"visibleWhen": "department = \"engineering\""
},
{
"value": "backend",
"label": "Backend Dev",
"visibleWhen": "department = \"engineering\""
},
{
"value": "ux",
"label": "UX Designer",
"visibleWhen": "department = \"design\""
},
{
"value": "visual",
"label": "Visual Designer",
"visibleWhen": "department = \"design\""
}
]
}
}
}
The role options update automatically when the department value changes. If the currently selected role becomes invisible, the library clears the selection.
Event System
The useForma hook exposes lifecycle events for side effects like analytics tracking, data injection before submission, and external state synchronization. Events do not trigger React re-renders and have zero overhead when no listeners are registered.
See the dedicated Events page for the full API.