Using #type 'details' for collapsible sectionsfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Picture a long insurance application: personal details, coverage options, payment info, all crammed onto one endless page. Nobody wants to scroll through that. Grouping related fields into collapsible sections turns an intimidating wall of inputs into a form people can actually digest one piece at a time — and Drupal's Form API gives you this for free with a single render array property.

What you'll learn in this lesson

  • How #type => 'details' creates a collapsible, expandable group of form elements
  • How nesting form elements inside a details element groups them visually and structurally
  • The difference between #open starting expanded vs. collapsed
  • Why grouping doesn't change how you read submitted values back out

Grouping elements with #type => 'details'

$form['shipping'] = [
  '#type' => 'details',
  '#title' => $this->t('Shipping address'),
  '#open' => TRUE,
];
$form['shipping']['street'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Street address'),
];
$form['shipping']['city'] = [
  '#type' => 'textfield',
  '#title' => $this->t('City'),
];

$form['billing'] = [
  '#type' => 'details',
  '#title' => $this->t('Billing address'),
  '#open' => FALSE,
];
$form['billing']['street'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Street address'),
];

Any element you nest as a child of a details element renders inside its collapsible body. The parent itself isn't a form input — it's purely a visual and structural container. Set #open => TRUE and the section starts expanded (good for the first, most important group); leave it FALSE and it starts collapsed, saving vertical space for sections most visitors won't need to touch.

Reading values back out — nothing changes

$street = $form_state->getValue(['shipping', 'street']);

Grouping is purely presentational. The nested array structure in buildForm() mirrors exactly how you retrieve the submitted value in submitForm() — a field nested under shipping is read with the same nested path, ['shipping', 'street'], whether or not #tree => TRUE is set higher up. The details wrapper doesn't add or remove a nesting level of its own; it just happens to share the same array key as its children's parent.

Fieldsets still exist too: #type => 'fieldset' groups elements the same way but renders as a plain, always-visible box with no collapse/expand behavior. Reach for details when a section is optional or secondary and you want to save space; reach for fieldset when a group should always be visible and collapsing it would hide something the user needs.

See it for yourself

Build a form with two details groups, one #open => TRUE and one #open => FALSE. Load the page and confirm the first group's fields are visible immediately while the second is collapsed behind a clickable summary — click it to expand.

Quick check: you nest a textfield under a details element named shipping, but you never explicitly set #tree => TRUE anywhere on the form. Will $form_state->getValue(['shipping', 'street']) still work? (Answer: yes — nesting a field under a container like details preserves that nesting in the submitted values structure on its own; #tree matters for whether *sibling* top-level values get flattened or not, not for whether a container's own children stay nested.)

Key takeaways

  • #type => 'details' groups form elements into a collapsible section; nest elements as children to place them inside it.
  • #open => TRUE starts the section expanded; FALSE starts it collapsed.
  • Use #title on the details element to label the collapsible section itself.
  • #type => 'fieldset' groups elements the same way but without collapse/expand behavior — always visible.
  • Reading nested values back with $form_state->getValue([...]) works the same whether the parent is a details element, fieldset, or container.

Coming up next

You've now covered every way to shape and configure a form's structure and appearance. Next, the tutorial turns to a whole new concern: validation — starting with implementing basic validation, the first line of defense between what a user types and what your module actually accepts.