Using #attributes for CSS classes or HTML attributesfor Drupal 11 , 10 , 9 , and 8

Last updated :  

Sometimes a form element needs a specific CSS class for styling, a data-* attribute for JavaScript to hook into, or an id a link elsewhere on the page needs to reference. #attributes is how you attach any of that directly onto an element's rendered HTML, without writing a custom template override just to add one class.

What you'll learn in this lesson

  • The #attributes array's structure
  • Adding classes without accidentally overwriting Drupal's own
  • Attributes on the wrapping element versus the input itself

Adding a CSS class

$form['priority'] = [
  '#type' => 'select',
  '#title' => $this->t('Priority'),
  '#options' => $options,
  '#attributes' => [
    'class' => ['priority-selector', 'js-priority-field'],
  ],
];
Class is always an array, even for one class. 'class' => ['priority-selector'], not 'class' => 'priority-selector' — Drupal merges array-form classes with whatever classes it already adds to the element (form-select, error-state classes on validation failure, and so on) rather than overwriting them. Passing a plain string here is a common early mistake that silently breaks Drupal's own default styling classes.

Data attributes for JavaScript

$form['country'] = [
  '#type' => 'select',
  '#title' => $this->t('Country'),
  '#options' => $options,
  '#attributes' => [
    'data-region-selector' => 'true',
    'aria-describedby' => 'country-help-text',
  ],
];

Any valid HTML attribute works here, not just class — a common pattern for a custom JavaScript behavior (from the Drupal.behaviors topic) that needs a reliable selector to attach to, without relying on a brittle CSS class name that a theme might change.

Wrapper attributes vs. input attributes

$form['name'] = [
  '#type' => 'textfield',
  '#title' => $this->t('Name'),
  '#attributes' => ['class' => ['input-styling']], // On the <input> itself.
  '#wrapper_attributes' => ['class' => ['field-wrapper-styling']], // On the surrounding wrapper.
];

Not every property targets the same HTML element — #attributes applies to the actual input/select/textarea tag, while #wrapper_attributes targets the surrounding container Drupal generates around it (which also holds the label and description). Styling that should affect spacing or layout around the whole field usually belongs on the wrapper, not the input.

Quick check: you set '#attributes' => ['class' => 'my-class'] (a plain string, not an array) on a required textfield. After a failed validation, does the field still get Drupal's automatic error-state styling? If you said it might not — a plain string can overwrite rather than merge with classes Drupal tries to add — you've spotted exactly the fragile pattern this lesson warns against.

Key takeaways

  • #attributes['class'] must always be an array, even for a single class, so Drupal can merge rather than overwrite its own classes.
  • Any valid HTML attribute — data attributes, ARIA attributes — can go in #attributes, not just class.
  • #attributes targets the input element itself; #wrapper_attributes targets the surrounding container.

Coming up next

You've controlled an element's HTML output. Next, controlling something structural instead: the order elements actually appear in.