FormControl

FormControl keeps the visible and semantic parts of one field together. Use it when a text field, select, combobox, or other control needs a label, required state, helper text, or validation message.
The wrapped control still owns its value and actions. FormControl owns the surrounding field anatomy and relates that anatomy to one unambiguous control.

Example

use fission::prelude::*;

let email_field: Widget = FormControl {
    id: Some(WidgetId::explicit("account.email-field")),
    label: Some("Email address".into()),
    child: TextInput {
        id: Some(WidgetId::explicit("account.email-input")),
        value: view.state().email.clone(),
        on_input: Some(update_email_action),
        ..Default::default()
    }
    .into(),
    error: view.state().email_error.clone(),
    helper: Some("Used for receipts and account alerts.".into()),
    required: true,
}
.into();
When error is present it replaces the helper message, marks the related control invalid, and supplies that validation message to accessibility output.
Use FormControlLayout when the anatomy needs localized text, custom retained presentation, stable region identities, or a helper that remains visible beside an error:
use fission::prelude::*;

let mut email = FormControlLayout::new(
    WidgetId::explicit("account.email-field"),
    TextInput {
        id: Some(WidgetId::explicit("account.email-input")),
        value: view.state().email.clone(),
        on_input: Some(update_email_action),
        ..Default::default()
    },
)
.label(
    FormLabel::custom(
        TextContent::KeyWithFallback {
            key: "account.email.label".into(),
            fallback: "Email address".into(),
        },
        email_label_node,
    )
    .semantics_identifier("account.email.label"),
)
.description(
    FormDescription::custom(
        "Used for receipts and account alerts.",
        receipt_explanation_node,
    )
    .semantics_identifier("account.email.description"),
)
.required(true);

if let Some(message) = &view.state().email_error {
    email = email.error(
        FormError::new(message.clone())
            .semantics_identifier("account.email.error"),
    );
}

let email_field: Widget = email.into();
The text passed to FormLabel::custom, FormDescription::custom, and FormError::custom remains the accessible text; the retained child controls only visual presentation. Standard constructors render the text with the active field-label or helper recipe.

Anatomy and visual states

The shared recipe arranges these regions in this vertical order:
1.
an optional label, including a visible * when required is true;
2.
the supplied child control;
3.
an optional helper description; and
4.
an optional validation error.
The compact FormControl chooses either its error or its helper. The named FormControlLayout can retain both: both describe the field, while only FormError supplies invalid state and the authoritative validation message. When a custom label child is supplied, that child owns the visual required marker; semantic required state is still propagated to the control.
Spacing comes from the active spacing tokens. Label and message typography use the label and helper styles from theme.components.text_input, which keeps the field anatomy consistent with TextInput. In error state, the visible label and message use the semantic error color. The child keeps its own layout and visual recipe; configure validation on that child too when its own painted border or surface must show an invalid state.

Field reference

Field
Type
Meaning
Notes
id
Option<WidgetId>
Stable identity for the complete field group.
When omitted, Fission derives an implicit identity from the retained build path.
label
Option<String>
Visible and semantic field label.
The spoken label excludes the visual required marker.
child
Widget
The input or choice control.
Required. Its value and actions remain controlled by the application.
error
Option<String>
Current validation message.
Takes precedence over helper and marks one related child control invalid.
helper
Option<String>
Guidance shown while there is no error.
Omit it when the label and context are sufficient.
required
bool
Whether the field is required.
Adds the visible marker and semantic required state.
The named anatomy has these responsibilities:
Type
Responsibility
FormControlLayout
Owns one required control plus optional label, description, error, and semantic required state. Its id is required.
FormLabel
Supplies accessible label text, optional custom retained presentation, and optional semantic ID and identifier.
FormDescription
Supplies persistent supporting text related through described_by, with optional custom presentation and identity.
FormError
Supplies a validation message, invalid state, described_by relationship, and optional custom presentation and identity.
Create named anatomy with FormControlLayout::new(id, control), then add regions with .label(...), .description(...), .error(...), and .required(...). Each text region has new(text) and custom(text, child) constructors plus .id(...) and .semantics_identifier(...). If an explicit region ID is unnecessary, the layout derives a stable one from its own ID.

Semantic relationships

The label and current messages receive stable identities derived from the layout id unless explicit IDs were supplied. The wrapper exposes labelled_by and described_by relationships, and those relationships are also added to the single eligible form-control semantic node inside child. Existing child relationships, identity, actions, value, selection, and text-editing behavior are preserved. When named anatomy contains both a description and an error, both IDs appear in described_by; only the error marks the control invalid.
Eligible roles are text input, generic input, combobox, checkbox, radio, switch, and slider. This includes the actual trigger inside Select and the editable input inside Combobox, rather than stopping at an outer layout node. Required state is additive. When error is present, that control exposes an invalid validation state and the same validation message.
If the child subtree contains no eligible control or more than one, Fission does not guess which descendant owns the label. The wrapper still exposes its own relationships, but the descendants are left unchanged. Use one FormControl per field instead of wrapping a group of unrelated inputs.

Keyboard, responsive, and target behavior

FormControl does not insert a focus stop or intercept keys. The wrapped control owns Tab, editing, selection, popup, and activation behavior. The label is descriptive text rather than an independently clickable focus target.
The column follows its parent's width and does not choose a breakpoint. Test long labels and validation messages, increased text scale, and narrow layouts. FormControl registers no motion itself. Any motion composed around the field still follows the global Env::motion_preference, so the control does not need its own preference branch.
Visible content and relationships lower through the shared target model. Static site and SSR output can emit the labelled, described, required, and invalid field contract; changing values or validation state still requires an interactive runtime or browser island.

Production checklist

•
Wrap exactly one field and give dynamic or reorderable fields a durable id.
•
Keep helper text useful and make validation messages explain how to recover.
•
Drive error from application validation state rather than local paint-only conditions.
•
Configure the child control's validation presentation too when its surface should visibly enter an error recipe.
•
Verify the resolved accessible name, description, required state, and invalid message on the real target bridge.