Skip to main content

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

PropertyTypeDescription
prefixstring | undefinedText displayed before the input
suffixstring | undefinedText 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:

StateAppearanceEditableValue SubmittedUse Case
readonlyClear, normal textNoYesConfirmed values, reference data
disabledGreyed outNoYesDependent fields not yet applicable

Props

  • field.readonlyboolean indicating the field is readonly
  • field.enabledboolean indicating 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

PropertyTypeDescription
variantstring | undefinedVariant name
variantConfigRecord<string, unknown> | undefinedVariant-specific configuration

These are available on all field types via BaseFieldProps.

Available Variants

Field TypeVariantDescriptionvariantConfig Keys
numbersliderRange slidershowValue (boolean)
numberstepperIncrement/decrement buttons-
numbernps0-10 NPS scalelowLabel, highLabel
numberrating-starsStar ratingmaxStars (default 5)
numberrating-numericNumeric rating buttonsmaxRating (default 5)
selectradioRadio button group-
selectradio-cardsCard-style radio options-
selectbutton-groupSegmented button group-
displayheadingSection headinglevel (2-5)
displaydividerVisual separator-
displaymetricLarge numeric displayunit
displayalertAlert with severityseverity ("info", "warning", "error", "success")
displaycalloutInformational callouticon (e.g., "info", "lightbulb")
displaysummaryCard-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.