Drupal Form Elements: A Tour of Every #type in the Form APIfor Drupal 11 , and 10

Last updated :  

You've now built a form with exactly one field. Real forms need much more: checkboxes, dates, dropdowns, file uploads, password confirmation. The good news is that you already know the pattern from the last lesson — every element is still just a PHP array with a #type key. This lesson is a tour through nearly every #type Drupal ships with, all demonstrated on a single real page.

What you'll learn in this lesson

  • How to declare over 25 different form element types, from checkboxes to file uploads
  • Which types are thin wrappers around plain HTML5 inputs, and which are Drupal-specific "compound" elements doing real work behind the scenes
  • How to inject a Drupal service into a form class using constructor dependency injection
  • How to read every submitted value back out in one pass with $form_state->getValues()
You don't need to memorize every type on this page today. Treat this lesson as a reference you'll come back to — bookmark it. The goal right now is just to recognize the pattern and know this catalog exists.

The source file

Path: modules/form_api_example/src/Form/InputDemo.php

<?php

namespace Drupal\form_api_example\Form;

use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Extension\ExtensionPathResolver;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Implements InputDemo form controller.
 *
 * This example demonstrates the different input elements that are used to
 * collect data in a form.
 *
 * @todo Change this class once either https://www.drupal.org/i/2940190 or
 *   https://www.drupal.org/i/2940481 are fixed.
 */
class InputDemo extends FormBase {

  /**
   * The extension path resolver.
   *
   * @var \Drupal\Core\Extension\ExtensionPathResolver
   */
  protected $extensionPathResolver;

  /**
   * Constructs a new \Drupal\form_api_example\Form\InputDemo object.
   *
   * @param \Drupal\Core\Extension\ExtensionPathResolver $extension_path_resolver
   *   The extension path resolver.
   */
  public function __construct(ExtensionPathResolver $extension_path_resolver) {
    $this->extensionPathResolver = $extension_path_resolver;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    $form = new static($container->get('extension.path.resolver'));
    $form->setMessenger($container->get('messenger'));
    $form->setStringTranslation($container->get('string_translation'));

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['description'] = [
      '#type' => 'item',
      '#markup' => $this->t('This example shows the use of all input-types.'),
    ];

    // CheckBoxes.
    $form['tests_taken'] = [
      '#type' => 'checkboxes',
      '#options' => ['SAT' => $this->t('SAT'), 'ACT' => $this->t('ACT')],
      '#title' => $this->t('What standardized tests did you take?'),
      '#description' => 'Checkboxes, #type = checkboxes',
    ];

    // Color.
    $form['color'] = [
      '#type' => 'color',
      '#title' => $this->t('Color'),
      '#default_value' => '#ffffff',
      '#description' => 'Color, #type = color',
    ];

    // Date.
    $now = new DrupalDateTime();
    $form['expiration'] = [
      '#type' => 'date',
      '#title' => $this->t('Content expiration'),
      '#default_value' => $now->format('Y-m-d'),
      '#description' => 'Date, #type = date',
    ];

    // Date-time.
    $form['datetime'] = [
      '#type' => 'datetime',
      '#title' => 'Date Time',
      '#date_increment' => 1,
      '#default_value' => $now,
      '#description' => $this->t('Date time, #type = datetime'),
    ];

    // URL.
    $form['url'] = [
      '#type' => 'url',
      '#title' => $this->t('URL'),
      '#maxlength' => 255,
      '#size' => 30,
      '#description' => $this->t('URL, #type = url'),
    ];

    // Email.
    $form['email'] = [
      '#type' => 'email',
      '#title' => $this->t('Email'),
      '#description' => $this->t('Email, #type = email'),
    ];

    // Number.
    $form['quantity'] = [
      '#type' => 'number',
      '#title' => $this->t('Quantity'),
      '#description' => $this->t('Number, #type = number'),
    ];

    // Password.
    $form['password'] = [
      '#type' => 'password',
      '#title' => $this->t('Password'),
      '#description' => 'Password, #type = password',
    ];

    // Password Confirm.
    $form['password_confirm'] = [
      '#type' => 'password_confirm',
      '#title' => $this->t('New Password'),
      '#description' => $this->t('PasswordConfirm, #type = password_confirm'),
    ];

    // Range.
    $form['size'] = [
      '#type' => 'range',
      '#title' => $this->t('Size'),
      '#min' => 10,
      '#max' => 100,
      '#description' => $this->t('Range, #type = range'),
    ];

    // Radios.
    $form['settings']['active'] = [
      '#type' => 'radios',
      '#title' => $this->t('Poll status'),
      '#options' => [0 => $this->t('Closed'), 1 => $this->t('Active')],
      '#description' => $this->t('Radios, #type = radios'),
    ];

    // Search.
    $form['search'] = [
      '#type' => 'search',
      '#title' => $this->t('Search'),
      '#description' => $this->t('Search, #type = search'),
    ];

    // Select.
    $form['favorite'] = [
      '#type' => 'select',
      '#title' => $this->t('Favorite color'),
      '#options' => [
        'red' => $this->t('Red'),
        'blue' => $this->t('Blue'),
        'green' => $this->t('Green'),
      ],
      '#empty_option' => $this->t('-select-'),
      '#description' => $this->t('Select, #type = select'),
    ];

    // Multiple values option elements.
    $form['select_multiple'] = [
      '#type' => 'select',
      '#title' => 'Select (multiple)',
      '#multiple' => TRUE,
      '#options' => [
        'sat' => 'SAT',
        'act' => 'ACT',
        'none' => 'N/A',
      ],
      '#default_value' => ['sat'],
      '#description' => 'Select Multiple',
    ];

    $form['phone'] = [
      '#type' => 'tel',
      '#title' => $this->t('Phone'),
      '#description' => $this->t('Tel, #type = tel'),
    ];

    $form['details'] = [
      '#type' => 'details',
      '#title' => $this->t('Details'),
      '#description' => $this->t('Details, #type = details'),
    ];

    // TableSelect.
    $options = [
      1 => ['first_name' => 'Indy', 'last_name' => 'Jones'],
      2 => ['first_name' => 'Darth', 'last_name' => 'Vader'],
      3 => ['first_name' => 'Super', 'last_name' => 'Man'],
    ];

    $header = [
      'first_name' => $this->t('First Name'),
      'last_name' => $this->t('Last Name'),
    ];

    $form['table'] = [
      '#type' => 'tableselect',
      '#title' => $this->t('Users'),
      '#header' => $header,
      '#options' => $options,
      '#empty' => $this->t('No users found'),
    ];

    // Textarea.
    $form['text'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Text'),
      '#description' => $this->t('Textarea, #type = textarea'),
    ];

    // Text format.
    $form['text_format'] = [
      '#type' => 'text_format',
      '#title' => 'Text format',
      '#format' => 'plain_text',
      '#description' => $this->t('Text format, #type = text_format'),
    ];

    // Textfield.
    $form['subject'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Subject'),
      '#size' => 60,
      '#maxlength' => 128,
      '#description' => $this->t('Textfield, #type = textfield'),
    ];

    // Weight.
    $form['weight'] = [
      '#type' => 'weight',
      '#title' => $this->t('Weight'),
      '#delta' => 10,
      '#description' => $this->t('Weight, #type = weight'),
    ];

    // Group submit handlers in an actions element with a key of "actions" so
    // that it gets styled correctly, and so that other modules may add actions
    // to the form.
    $form['actions'] = [
      '#type' => 'actions',
    ];

    // Extra actions for the display.
    $form['actions']['extra_actions'] = [
      '#type' => 'dropbutton',
      '#links' => [
        'simple_form' => [
          'title' => $this->t('Simple Form'),
          'url' => Url::fromRoute('form_api_example.simple_form'),
        ],
        'demo' => [
          'title' => $this->t('Build Demo'),
          'url' => Url::fromRoute('form_api_example.build_demo'),
        ],
      ],
    ];

    // File.
    $form['file'] = [
      '#type' => 'file',
      '#title' => 'File',
      '#description' => $this->t('File, #type = file'),
    ];

    // Manage file.
    $form['managed_file'] = [
      '#type' => 'managed_file',
      '#title' => 'Managed file',
      '#description' => $this->t('Manage file, #type = managed_file'),
    ];

    // Image Buttons.
    $form['image_button'] = [
      '#type' => 'image_button',
      '#value' => 'Image button',
      '#src' => $this->extensionPathResolver->getPath('module', 'examples') . '/images/button.svg',
      '#description' => $this->t('image file, #type = image_button'),
      '#attributes' => [
        'width' => 88,
      ],
    ];

    // Button.
    $form['button'] = [
      '#type' => 'button',
      '#value' => 'Button',
      '#description' => $this->t('Button, #type = button'),
    ];

    // Add a submit button that handles the submission of the form.
    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
      '#description' => $this->t('Submit, #type = submit'),
    ];

    // Add a reset button that handles the submission of the form.
    $form['actions']['reset'] = [
      '#type' => 'button',
      '#button_type' => 'reset',
      '#value' => $this->t('Reset'),
      '#description' => $this->t('Submit, #type = button, #button_type = reset, #attributes = this.form.reset();return false'),
      '#attributes' => [
        'onclick' => 'this.form.reset(); return false;',
      ],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'form_api_example_input_demo_form';
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // Find out what was submitted.
    $values = $form_state->getValues();
    foreach ($values as $key => $value) {
      $label = $form[$key]['#title'] ?? $key;

      // Many arrays return 0 for unselected values so lets filter that out.
      if (is_array($value)) {
        $value = array_filter($value);
      }

      // Only display for controls that have titles and values.
      if ($value && $label) {
        $display_value = is_array($value) ? preg_replace('/[\n\r\s]+/', ' ', print_r($value, 1)) : $value;
        $message = $this->t('Value for %title: %value', ['%title' => $label, '%value' => $display_value]);
        $this->messenger()->addMessage($message);
      }
    }
  }

}

How it works

Injecting a service with the constructor

public function __construct(ExtensionPathResolver $extension_path_resolver) {
  $this->extensionPathResolver = $extension_path_resolver;
}

public static function create(ContainerInterface $container) {
  $form = new static($container->get('extension.path.resolver'));
  $form->setMessenger($container->get('messenger'));
  $form->setStringTranslation($container->get('string_translation'));
  return $form;
}

This form needs to know the filesystem path of a module (to build an image button's URL), so it injects Drupal's extension.path.resolver service. The pattern is always the same: declare a typed constructor parameter, store it on a property, and implement a static create() method that pulls the actual service out of Drupal's service container by its ID. Whenever you see \Drupal::service('something') called directly inside a class method, that's usually a sign the class should be using constructor injection like this instead — it's more testable and makes a class's dependencies explicit.

The element catalog

Every one of the following is declared exactly the same way you saw in the last lesson: an array key becomes the field name, and a #type property picks the widget. Group them by family and this list gets much less intimidating:

Plain HTML5 inputs, with Drupal validation layered on top

  • textfield — a single line of text (#maxlength, #size)
  • textarea — multiple lines of plain text
  • url, email, tel, search — semantic text inputs; url and email get real server-side format validation, not just a browser hint
  • number, range — numeric input and a slider, both accept #min/#max
  • color — the browser's native color picker; #default_value must be a #rrggbb hex string
  • date — an HTML5 date picker (#default_value as a 'Y-m-d' string)
  • password — a masked text field that never re-populates on reload
  • file — a raw upload input; Drupal gives you the uploaded data but does not save it anywhere automatically

Choice elements

  • checkboxes (plural) — a group of independent checkboxes from #options; submitted as an array where unchecked items come back as 0
  • radios — mutually-exclusive options; submitted value is the key of the one selected
  • select — a dropdown; add #multiple => TRUE for a multi-select listbox, or #empty_option for a "choose one" placeholder

Drupal-specific "compound" elements — these do real work for you

  • datetime — a date and time picker together; unlike plain date, it accepts a full DrupalDateTime object as its default value and is timezone-aware
  • password_confirm — renders two password boxes and automatically validates that they match, with zero extra code from you
  • text_format — a textarea paired with a text-format selector (plain text, basic HTML, full HTML); submits as an array with value and format keys
  • tableselect — a full data table where every row has a checkbox, built from #header and #options arrays — this is the same element Drupal's own admin bulk-action pages use
  • managed_file — an AJAX-powered upload widget that (unlike plain file) actually saves the upload as a tracked Drupal file entity and returns its file ID
  • weight — a dropdown pre-filled with integers from -#delta to +#delta, used throughout Drupal's admin UI for drag-and-drop-style ordering
  • details — a collapsible <details>/<summary> section that can contain other form elements as children

Actions and buttons

  • submit — runs full validation, then calls submitForm()
  • button — does not trigger full-form submission by default; here it's reused with #button_type => 'reset' plus a small inline onclick to clear the form client-side
  • dropbutton — a primary action with a dropdown of secondary links, each needing a title and a url built with Url::fromRoute()
  • image_button — an <input type="image">; its #src here is built dynamically using the injected extensionPathResolver
Notice the radios element is declared at $form['settings']['active'] instead of a flat top-level key. Nesting keys like this is purely organizational — Drupal reads and renders elements at any depth, and it doesn't change how you submit or retrieve the value.

Reading everything back at once

public function submitForm(array &$form, FormStateInterface $form_state) {
  $values = $form_state->getValues();
  foreach ($values as $key => $value) {
    $label = $form[$key]['#title'] ?? $key;
    if (is_array($value)) {
      $value = array_filter($value);
    }
    if ($value && $label) {
      $message = $this->t('Value for %title: %value', ['%title' => $label, '%value' => $display_value]);
      $this->messenger()->addMessage($message);
    }
  }
}

Instead of calling $form_state->getValue('key') once per field like the previous lesson did, this loops over the whole getValues() array at once — a handy pattern when a form has many fields and you want to process them generically. array_filter() strips out the 0s that unchecked checkboxes options leave behind, so only genuinely-filled-in fields produce a message.

See it for yourself

Visit /examples/form-api-example/input-demo on your DDEV site to see every element on this page rendered together.

The Input Demo form showing many Drupal Form API element types rendered together

Quick check: which element type on this page validates that two password fields match, without you writing any comparison code yourself? password_confirm — it's a compound element that handles its own internal validation.

Key takeaways

  • Every form element — from a plain textfield to a full data table — is declared the same way: an array key plus a #type property. Drupal renders, validates, and secures all of them for you.
  • Prefer Drupal's semantic HTML5 types (url, email, tel, search) over plain textfield when the data has a specific shape — you get real server-side validation, not just a browser hint.
  • Compound elements like password_confirm, text_format, and datetime render multiple widgets but manage their own internal logic and validation — use them instead of reinventing that behavior yourself.
  • Use constructor injection (a typed constructor parameter plus a static create() method) to access Drupal services in a form class — avoid calling \Drupal::service() directly inside methods when injection is available.
  • managed_file tracks uploads as real Drupal file entities; plain file just hands you raw upload data with no automatic storage or cleanup.
  • $form_state->getValues() retrieves every submitted value in one array — useful for looping generically, versus calling getValue('key') field by field.

Coming up next

Knowing every element type is only half the job — the other half is making sure what people type is actually valid before you do anything with it. The next lesson goes deeper into server-side validation: how error messages attach to specific fields, and what happens to the form when validation fails.