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:

TextInput.tsx
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​

{
"fields": {
"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.readonly — boolean indicating the field is readonly
  • field.enabled — boolean indicating the field is enabled (inverse of disabled)

Implementation​

ReadonlyTextInput.tsx
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-
matrixlikertLikert scale layout-
matrixdropdown-gridDropdowns per row-

Implementation Pattern​

Check the variant and route to the appropriate sub-component, falling back to the default:

NumberField.tsx
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​

{
"fields": {
"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:

{
"fields": {
"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:

SelectField.tsx
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.