dify/packages/dify-ui/docs/forms.md

4.1 KiB

Forms

Dify UI form primitives compose Base UI's native form semantics, field accessibility, and Dify styling. They are not a form state-management or schema framework. See the Base UI forms handbook for the upstream model.

Submit boundary

Every group of controls that saves or submits together needs a real <form> boundary. Do not wire an Input and a click-only Button together as an informal form.

Use Form when the Dify UI boundary should own Base UI's structured onFormSubmit values, consolidated errors, actionsRef, or validationMode. It renders a native <form>. A native form remains correct when another form library owns submission and validation; do not nest form owners.

Set Button submit buttons to type="submit" explicitly. Keep every other button inside a form at type="button".

Value and state ownership

Form owns the submission and validation boundary described above, not each field's draft.

Choose controlledness from source-of-truth needs, independently from where a draft is stored. Prefer defaultValue when application React code does not need to own the current value. Use value and change handlers when application rendering or coordination must own it while editing. Listening to change events, tracking dirty state, and native or primitive validation do not by themselves require controlled state.

Application code owns the draft in the narrowest component whose lifetime matches it. A value can be controlled locally without being lifted. An uncontrolled field can participate in a persisted workflow when that workflow captures its value at an explicit persistence boundary. The surrounding surface defines its mount lifecycle; owner placement determines whether draft state lives inside or outside that lifecycle.

Fields and labels

Use Field when a control needs a shared name, label, validation, description, or error state. A standalone Input may use a native <label htmlFor> relationship, but normal form rows should prefer a visible label. FieldDescription and FieldError provide the corresponding accessible message relationships.

Choose the label primitive by the control:

  • Text-like inputs, Textarea, input-based Combobox and Autocomplete, a single Checkbox, each Radio option, Switch, and NumberField use FieldLabel.
  • Trigger-based Select fields use SelectLabel.
  • Slider fields follow the Base UI Slider anatomy and use SliderLabel; only multi-thumb sliders add per-thumb aria-label to distinguish the thumbs.
  • SelectGroupLabel and AutocompleteGroupLabel label option groups inside popup content. They are not field labels.

Use InputGroup when a prefix, suffix, or action shares the input's visual surface.

Grouped controls

Use Fieldset and FieldsetLegend when one field contains related controls, such as checkbox or radio groups, multi-thumb sliders, or a section of related inputs. Wrap each checkbox or radio option with FieldItem and give it its own label:

<Field name="allowedNetworkProtocols">
  <Fieldset render={<CheckboxGroup />}>
    <FieldsetLegend>Allowed network protocols</FieldsetLegend>
    <FieldItem>
      <FieldLabel className="flex items-center gap-2">
        <Checkbox value="https" />
        HTTPS
      </FieldLabel>
    </FieldItem>
  </Fieldset>
</Field>

Fieldset owns group semantics and the legend relationship, not interactive state. Pass disabled, value, defaultValue, and change handlers to the group primitive.

Every radio belongs to a RadioGroup. Use FieldsetLegend to name the group and FieldLabel to name each option; do not render a standalone Radio.

Keep form state, schemas, server validation, and reset behavior outside the primitive internals, in the nearest application owner with the required lifetime. Pass observable state through the public field and control props instead of replacing the semantic structure.