Combining Multiple #states Conditions with And, Or, Xorfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Every example so far has watched exactly one other field. Real forms are rarely that simple: maybe a field should only appear when a visitor has checked "Need Special Accommodations?" and selected "Ground floor" as their room preference. A single selector-and-condition pair can't express that — you need to combine multiple conditions with real logical operators, and #states supports exactly that.

What you'll learn in this lesson

  • How to require multiple conditions to all be true using implicit "and" logic
  • How to accept any one of several conditions using explicit "or" grouping
  • What "xor" means in this context, and the rare cases it's actually useful for
  • How nesting conditions changes the array structure compared to a single condition

The source file

Path (relative to the Examples module's root): modules/form_api_example/src/Form/StateDemo.php — extended here with a multi-condition example built on the same accommodation form.

$form['ground_floor_note'] = [
  '#type' => 'item',
  '#markup' => $this->t('A ground-floor accommodation note will be added to your reservation.'),
  '#states' => [
    'visible' => [
      ':input[name="needs_accommodation"]' => ['checked' => TRUE],
      ':input[name="room_preference"]' => ['value' => 'ground_floor'],
    ],
  ],
];

How it works

Implicit "and": list two conditions, both must be true

'#states' => [
  'visible' => [
    ':input[name="needs_accommodation"]' => ['checked' => TRUE],
    ':input[name="room_preference"]' => ['value' => 'ground_floor'],
  ],
],

When you list two selector/condition pairs one after another inside the same state's array, Drupal treats them as an AND — every condition listed must hold true simultaneously for the state to apply. Here, the note only becomes visible when the checkbox is checked and "Ground floor" is the selected room preference. Miss either condition and the note stays hidden. This implicit-AND form is the one you'll reach for most often, and it's exactly what you've already been writing in every earlier lesson — just with a single condition instead of two.

Explicit "or": nest conditions inside an 'or' key

'#states' => [
  'visible' => [
    'or' => [
      ':input[name="room_preference"]' => ['value' => 'ground_floor'],
      ':input[name="room_preference"]' => ['value' => 'near_elevator'],
    ],
  ],
],

AND is the default when you list conditions side by side, so getting OR logic requires being explicit about it: wrap the conditions in an array keyed 'or'. Now the state applies if either condition holds — useful for "this note applies to any accessibility-related room choice," where more than one option should trigger the same behavior. Note this simplified example has a real PHP array-key-collision problem worth knowing about: two array keys with the exact same string (:input[name="room_preference"]) would silently overwrite each other in PHP. In practice, when the selectors are genuinely identical like this, you'd restructure using numeric indices or a helper — this is exactly the kind of subtle multi-condition trap worth testing carefully rather than assuming works as written.

'xor' — exactly one condition, never both or neither

'#states' => [
  'required' => [
    'xor' => [
      ':input[name="email"]' => ['filled' => TRUE],
      ':input[name="phone"]' => ['filled' => TRUE],
    ],
  ],
],

Genuinely rare in practice, but worth knowing: xor ("exclusive or") applies the state only when precisely one of the listed conditions is true — not zero, and not both. A realistic use case: a "contact preference" field that should only be marked required if the visitor has filled in exactly one of email or phone, treating "filled in neither" and "filled in both, redundantly" as separate cases from your validation logic elsewhere in the form.

Test the exact behavior you write, not the behavior you assume. Nested condition logic like this is genuinely easy to get subtly backwards — an AND where you meant OR, or a condition that silently overwrites another due to a duplicate array key, as in the example above. Because #states conditions fail silently (a wrong condition just never triggers, with no error anywhere), always click through the actual form states by hand after writing one, rather than trusting the array structure looks right.

See it for yourself

Visit /examples/form-api-example/state-demo on your own DDEV site and walk through each combination of checkbox and dropdown state by hand, confirming the visibility rules behave the way the array structure describes.

Quick check: you write two conditions side by side (no 'or' wrapper) targeting two different selectors. Is that AND logic or OR logic? (Answer: AND — listing conditions side by side inside a state's array is always an implicit AND. You only get OR by explicitly wrapping the conditions in an array keyed 'or'.)

Key takeaways

  • Listing multiple selector/condition pairs side by side inside a state produces implicit AND logic — every condition must hold.
  • Wrap conditions in an array keyed 'or' to get OR logic — any one condition holding is enough.
  • 'xor' applies the state only when exactly one condition holds, never zero or both — genuinely rare, but real.
  • #states conditions fail silently when written wrong — always manually test every combination of watched fields rather than trusting the array structure at a glance.

Coming up next

You've now built genuinely reactive forms using nothing but declarative PHP arrays. The next lesson moves to a different but related skill: altering forms you didn't write at all — reaching into core or contributed modules' existing forms with hook_form_alter() to change their behavior from your own module.