Form States System (#states)
When to Use
Use #states for client-side show/hide and enable/disable. Use AJAX when server-side logic or dynamic options needed.
Decision
| Situation | Choose | Why |
|---|---|---|
| Simple show/hide logic | #states | Instant, client-side only |
| Field enable/disable | #states | No server call needed |
| Required status changes | #states | Client-side validation |
| Options must change | AJAX | Need server data |
| Need server-side data | AJAX | Complex logic |
| Need to load entities | AJAX | Server processing required |
Supported States
Visibility: visible, invisible
Interaction: enabled, disabled, readonly
Validation: required, optional
Special: checked, unchecked (checkbox), expanded, collapsed (details)
Basic Syntax
// Show field when checkbox checked
$form['custom_value'] = [
'#type' => 'textfield',
'#title' => $this->t('Custom Value'),
'#states' => [
'visible' => [
':input[name="use_custom"]' => ['checked' => TRUE],
],
'required' => [
':input[name="use_custom"]' => ['checked' => TRUE],
],
],
];
// Show based on select value
$form['other_value'] = [
'#type' => 'textfield',
'#title' => $this->t('Other'),
'#states' => [
'visible' => [
'select[name="type"]' => ['value' => 'other'],
],
],
];
// Disable when another field empty
$form['dependent'] = [
'#type' => 'textfield',
'#states' => [
'disabled' => [
':input[name="primary"]' => ['empty' => TRUE],
],
],
];
Trigger Conditions
| Condition | When True |
|---|---|
['checked' => TRUE] |
Checkbox is checked |
['unchecked' => TRUE] |
Checkbox is not checked |
['value' => 'foo'] |
Exact value match |
['value' => ['foo', 'bar']] |
Value in array |
['!value' => 'foo'] |
Value NOT equal |
['empty' => TRUE] |
Field is empty |
['filled' => TRUE] |
Field has value |
Selector Syntax
// Simple field
':input[name="field_name"]'
// Nested field
':input[name="container[field]"]'
// Field API widget
':input[name="field[0][value]"]'
// Radio buttons
':input[name="field_name"]' => ['value' => 'option1']
// Select dropdown
'select[name="field_name"]' => ['value' => 'option1']
// Checkboxes group
':input[name="field_name[option1]"]' => ['checked' => TRUE]
Complex Conditions
AND (all must be true):
'#states' => [
'visible' => [
':input[name="enable"]' => ['checked' => TRUE],
':input[name="role"]' => ['value' => 'admin'],
],
],
OR (any can be true):
'#states' => [
'visible' => [
[':input[name="type"]' => ['value' => 'custom']],
'or',
[':input[name="type"]' => ['value' => 'advanced']],
],
],
XOR (exactly one true):
'#states' => [
'visible' => [
[':input[name="option1"]' => ['checked' => TRUE]],
'xor',
[':input[name="option2"]' => ['checked' => TRUE]],
],
],
Nested (AND + OR):
'#states' => [
'visible' => [
[':input[name="enable"]' => ['checked' => TRUE]],
'and',
[
[':input[name="type"]' => ['value' => 'custom']],
'or',
[':input[name="type"]' => ['value' => 'advanced']],
],
],
],
States vs AJAX Decision
Use #states when: - Simple show/hide logic - Field enable/disable - Required status changes - No server-side processing needed - Performance important
Use AJAX when: - Options must change (dependent dropdowns) - Need server-side data - Complex validation logic - Field structure must change - Need to load entities/data
Combine both: - #states for instant UI feedback - AJAX for data loading - Best user experience
Common Mistakes
- Wrong: Missing :input in selector → Right: Always use
:input[name="field"] - Wrong: Nested field with dot notation → Right: Use brackets
container[field] - Wrong: Separate conditions for OR → Right: Use array + 'or' operator
- Wrong: String instead of array → Right:
[['field' => 'value']]not'field' => 'value'