Making Form Fields Required Conditionally with #statesfor Drupal 8 , 9 , 10 , and 11

Last updated :  

The dietary-restrictions field from earlier in this section is optional right up until it becomes essential: once a visitor says they need special accommodation, leaving that field blank isn't a minor omission anymore — it's a gap that could genuinely affect their stay. The required state lets a field's required-ness track another field's value, and it's the one #states behavior that reaches all the way into Drupal's validation system, not just the visual layer.

What you'll learn in this lesson

  • How #states => ['required' => ...] differs from every other state you've met so far
  • Why this is the one case where #states genuinely does affect server-side validation, not just appearance
  • How the visible asterisk and the enforced validation stay in sync automatically
  • A common pairing: making a field required at the same moment it becomes visible

The source file

Path (relative to the Examples module's root): modules/form_api_example/src/Form/StateDemo.php — extended here to make the dietary-restrictions field conditionally required.

$form['accommodation']['diet'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Dietary Restrictions'),
  '#states' => [
    'required' => [
      ':input[name="needs_accommodation"]' => ['checked' => TRUE],
    ],
  ],
];

How it works

The one state that isn't purely cosmetic

'#states' => [
  'required' => [
    ':input[name="needs_accommodation"]' => ['checked' => TRUE],
  ],
],

Every other state you've met — visible, invisible, disabled — only changes what happens in the browser. required is different: Drupal's Form API actually checks this condition again on the server during validation, not just in the client-side JavaScript. That means a visitor can't simply disable JavaScript and submit the form with the dietary field left blank while needs_accommodation is checked — the server independently re-evaluates the same condition and rejects the submission if it's not met. This is the one #states behavior that genuinely doubles as both a UX affordance and a real server-side validation rule, with zero extra code from you.

The asterisk and the enforcement move together

Watch the field's label closely in the browser: the moment needs_accommodation gets checked, a red required-field asterisk appears next to "Dietary Restrictions," in perfect sync with the server-side rule taking effect. You don't add that asterisk yourself — Drupal's render system draws it automatically for any element whose current required-state (static or dynamic) evaluates to true at render or re-render time.

Pairing required with invisible

$form['accommodation']['diet'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Dietary Restrictions'),
  '#states' => [
    'required' => [
      ':input[name="needs_accommodation"]' => ['checked' => TRUE],
    ],
  ],
];

// The parent container from earlier lessons still controls visibility.
$form['accommodation']['#states'] = [
  'invisible' => [
    ':input[name="needs_accommodation"]' => ['checked' => FALSE],
  ],
];

A field that's required but still hidden would be a genuinely confusing dead end for a visitor — they'd get a validation error for a field they can't even see. That's why these two states are almost always paired on the same condition: the field becomes visible and required at exactly the same moment, so the two behaviors always agree with each other.

Watch for the inverse mistake: if you make a field required whenever a checkbox is checked, but forget to also make it not required when the checkbox is unchecked, you can end up with a field that stays required even after the visitor unchecks the box — because your condition only ever described the "on" case. In practice this usually isn't a problem, since #states conditions are evaluated continuously and the required state simply stops applying once the condition (checked => TRUE) no longer matches — but it's worth testing both directions explicitly rather than assuming.

See it for yourself

Visit /examples/form-api-example/state-demo on your own DDEV site. Check "Need Special Accommodations?", leave the Dietary Restrictions field blank, and click Submit — Drupal blocks the submission with a validation error, exactly as if you'd written #required => TRUE by hand.

Quick check: why does required get real server-side enforcement while visible and disabled don't? (Answer: because "was this field required" is a rule Drupal's core validation logic already needs to check for every field regardless of #states — so extending that same check to also look at the dynamic condition was a natural, low-cost addition. Visibility and interactivity, by contrast, are purely presentational concerns that core's validation pipeline has no reason to care about.)

Key takeaways

  • #states => ['required' => ...] is the one state that's enforced server-side during validation, not just applied as a client-side visual cue.
  • The required-field asterisk and the actual validation rule stay in sync automatically — you never manage them separately.
  • Pair a conditionally-required field's condition with the same condition used to make it visible, so a visitor is never asked to fill in a field they can't see.
  • Test both directions of the condition (on and off) rather than assuming the "off" case is handled correctly by default.

Coming up next

You now know all three individual states — visible, disabled, required. The final lesson in this section shows you how to combine multiple conditions on a single state using and, or, and xor logic, for cases where one condition alone isn't specific enough.