Last updated 11 October 2016. Created on 24 March 2010.
Edited by jp.stacey, mcdruid, ashish_nirmohi, dddave. Log in to edit this page.

Note that these instructions focus on Drupal 6. Corresponding documentation for Drupal 7 is here.

The Drupal Form API provides sophisticated form techniques and also allows for almost unlimited possibilities for custom theming, validation, and execution of forms. Even better, ANY form (even those in core) can be altered in almost any way imaginable—elements can be removed, added, and rearranged.

Perhaps most important, the Form API provides a secure framework for forms, protecting against many exploits, and the programmer has to do almost nothing to get this protection.

This page is certainly not a comprehensive guide to this functionality. It should provide a good working foundation with which to do the most basic form creation, theming, validation, and execution. For programming details on form elements and their properties, please see the Drupal 6 Forms API Reference.

Jump to sections on creating, theming, validating, submitting and understanding the flow.

Creating Forms

Form elements are now declared in array fashion, with the hierarchical structure of the form elements themselves as array elements (which can be nested), and each form element's properties/attributes listed as array elements in key/value pairs—the key being the name of the property/attribute, and the value being the value of the property/attribute. For example, here's how to go about constructing a textfield form element:

$form['foo'] = array(
  '#type' => 'textfield',
  '#title' => t('bar'),
  '#default_value' => t('foo'),
  '#size' => 60,
  '#maxlength' => 64,
  '#description' => t('baz'),

Create a submit button:

$form['submit'] = array(
  '#type' => 'submit',
  '#value' => t('Save'),

a few things to note:

  1. The element's name property is declared in the $form array, at the very end of the array tree. For example, if an element in the form tree was structured like this:

    ...then that element's name property is 'username'--this is the key it will be available under in $form_state['values'], in your validation and submission functions, as the form code flattens the array in this fashion before it passes the key/value pairs. NOTE: if you wish to have the full tree structure passed to $form_state['values'], this is possible, and will be discussed later.

  2. The type of form element is declared as an attribute with the '#type' property.
  3. Properties/attributes keys are declared with surrounding quotes, beginning with a # sign. Values are strings.
  4. The order of the properties/attributes doesn't matter, and any attributes that you don't need, don't need to be declared. Many properties/attributes also have a default fallback value if not explicitly declared.
  5. Don't use the '#value' attribute for any form element that can be changed by the user. Use the '#default_value' attribute instead. Don't put values from $form_state['values'] (or $_POST) here! FormsAPI will deal with that for you; only put the original value of the field here.

One great advantage of this system is that the explicitly named keys make deciphering the form element much easier.

Let's take a look at a working piece of code using the API:

function test_myform(&$form_state) {
  // Access log settings:
  $options = array('1' => t('Enabled'), '0' => t('Disabled'));
  $form['access'] = array(
    '#type' => 'fieldset',
    '#title' => t('Access log settings'),
    '#tree' => TRUE,
  $form['access']['log'] = array(
    '#type' => 'radios',
    '#title' => t('Log'),
    '#default_value' =>  variable_get('log', 0),
    '#options' => $options,
    '#description' => t('The log.'),
  $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
  $form['access']['timer'] = array(
    '#type' => 'select',
    '#title' => t('Discard logs older than'),
    '#default_value' => variable_get('timer', 259200),
    '#options' => $period,
    '#description' => t('The timer.'),
  // Description
  $form['details'] = array(
    '#type' => 'fieldset',
    '#title' => t('Details'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  $form['details']['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Describe it'),
    '#default_value' =>  variable_get('description', ''),
    '#cols' => 60,
    '#rows' => 5,
    '#description' => t('Log description.'),
  $form['details']['admin'] = array(
    '#type' => 'checkbox',
    '#title' => t('Only admin can view'),
    '#default_value' => variable_get('admin', 0),
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#size' => 30,
    '#maxlength' => 64,
    '#description' => t('Enter the name for this group of settings'),
  $form['hidden'] = array('#type' => 'value', '#value' => 'is_it_here');
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
  return $form;

function test_page() {
  return drupal_get_form('test_myform');

drupal_get_form(). Note that the form builder function always takes $form_state as its first argument, though for basic usage (as here) it is not used. Also note that the test_page() function that renders the form is returning the rendered HTML output, which is what you would need to do if test_page() is the page callback from a hook_menu() implementation, for example.

Notice that the first layer is made up of two form groups, 'access', and 'details', and that inside each of these groups, one layer down, are some individual form elements. Order of construction is important here, as the form building code will default to the constructed order of the $form array when it builds the form (this can be overridden, and will be discussed later in the custom theming section).

For form groups, the '#type' parameter is set to 'fieldset', and notice how the 'details' form group is made into a collapsed form group with the addition of a few attributes.

All groups/elements are being built into the master $form array by the builder function.

The drupal_get_form function is the "key" function in the Form API. Note that in its basic usage, it takes just one argument, a string which is both the form ID and also the name of the function that builds the $form array. Because the form ID is generally also the name of a function, it must be a valid PHP variable name. It should start with a letter or underscore, followed by any number of letters, numbers, or underscores; spaces and hyphens are not allowed. drupal_get_form can take optional additional arguments, which will be simply passed on to the $form builder function.

drupal_get_form does the following:

  • Starts the entire form-building process by getting the $form from the builder function
  • Translates the $form['name'] items into actual form elements
  • Performs any validation and "clean-up" that needs to be done, and calls custom validation functions if declared
  • Submits the form if a submit function is declared, and the form has been submitted
  • Calls any custom theming functions that have been declared
  • Returns an HTML string which contains the actual form.

For more detailed information, see the API page for drupal_get_form().

An important thing to note: notice that $form['access'] has a '#tree' => TRUE attribute. this setting retains the full tree structure for all elements under it when it is passed to $form_state['values']. you must explicitly declare this anywhere you wish to retain an array's full hierarchy when it is passed.

Theming Forms

The API makes custom theming of all forms (including those found in core) possible. This custom theming becomes possible when all hard coded theming elements have been abstracted, so that they can be overridden at time of form generation. The abstraction is accomplished using one of the following methods:

  1. Adding '#theme' attributes to the form and/or elements. This allows you to specify which theme function will be used to render the form or elements, overriding the default theming function.
  2. Including any markup directly as an element in the $form array:
    • There are '#prefix' and '#suffix' attributes, and these will place the declared markup either before or after the form element in question. for example:
      $form['access'] = array(
        '#type' => 'fieldset',
        '#title' => t('Access log settings'),
        '#prefix' => '<div class="foo">',
        '#suffix' => '</div>',

      ...will place the div tags before and after the entire form group (meaning the form elements of the group will also be enclosed in the div). if you were to put those attributes in one of the form elements inside that form group, then they would only wrap that particular element, etc.

    • There is a '#markup' type which you can place anywhere in the form, and its value will be output directly in its specified location in the forms hierarchy when the form is rendered. example:
      $form['div_tag'] = array(
        '#type' => 'markup', 
        '#value' => '<div class="foo">',

      This markup form element can then be accessed/altered through its name in the array, 'div_tag'

      NOTE: it's not necessary to explicitly declare the #type at all, since #type will default to 'markup' if none is declared.

  3. Break out any markup into a separate theme function. This is the preferred method if the markup has any degree of complication. It is accomplished by creating a theme function with theme_ prepended to the name of the form ID that is to be themed. in cases where you want to use the same theming function for more than one form, you can include the optional callback arg in drupal_get_form--in which case the third arg of drupal_get_form will be a string containing the name of the callback function which the form building code will call, and the theming function will be theme_ prepended to the name of the callback.


    For our above form, we could create a custom theming function as follows:

    function theme_test_myform($form) {
      $output = '';
      $output .= drupal_render($form['name']);
      $output .= '
    '; $output .= drupal_render($form['access']); $output .= '
    '; $output .= drupal_render($form['details']); $output .= '
    '; $output .= drupal_render($form); return $output; }

    A few things to note:

    1. The theme function has one argument, which is the form array that it will theme
    2. You build and return an output string just as you would do in a regular theming function
    3. Form elements are rendered using the drupal_render function
    4. If you call drupal_render
      and pass it an array of elements (as in a fieldset), it will render all the elements in the passed array, in the order in which they were built in the form array.
    5. While the default order of rendering for a form is the order in which it was built, you can override that in the theme function by calling drupal_render for any element in the place where you would like it to be rendered. In the above example, this was done with $form['name'].
    6. The rendering code keeps track of which elements have been rendered, and will only allow them to be rendered once. Notice that drupal_render is called for the entire form array at the very end of the theming function, but it will only render the remaining unrendered element, which in this case is the submit button. calling drupal_render($form) is a common way to end a theming function, as it will then render any submit buttons and/or hidden fields that have been declared in the form in a single call.
    7. Don't forget to register the theme function by implementing hook_theme().

Validating Forms

The form API has general form validation which it performs on all submitted forms. If there is additional validation you wish to perform on a submitted form, you can create a validation function. the name of the validation function is the form ID with _validate appended to it. the function has two args: $form and $form_state. $form is the form array of the executed form, and $form_state['values'] contains the form values which you may perform validation on. (Note - in more advanced usage, several forms may share a _validate or _submit function - so if the form's ID is needed, it can be retrieved from $form['form_id']['#value'], or $form_state['values']['form_id'].)

Here's an example validation function for our example code:

function test_myform_validate($form, &$form_state) {
  if ($form_state['values']['name'] == '') {
    form_set_error('', t('You must select a name for this group of settings.'));

Submitting Forms

The normal method of submitting forms with the API is through the use of a form submit function. This has the same naming convention and arguments as the validation function, except _submit is appended instead. Any forms which are submitted from a button of type => 'submit' will be passed to their corresponding submit function if it is available.


function test_myform_submit($form, &$form_state) {
  db_query("INSERT INTO {table} (name, log, hidden) VALUES ('%s', %d, '%s')", $form_state['values']['name'], $form_state['values']['access']['log'],  $form_state['values']['hidden']);
  drupal_set_message(t('Your form has been saved.'));

a few things to note:

  1. A submit function is called only if a submit button was present and exists in the $_POST, and validation did not fail.
  2. The $form_state['values'] array will not usually have the same hierarchical structure as the constructed $form array (due to the flattening discussed previously), so be aware of what arrays have been flattened, and what arrays have retained their hierarchy by use of the tree => TRUE attribute. notice above that 'statistics_enable_access_log' belongs to a tree'd array, and the full array structure must be used to access the value.
  3. If a form has a submit function, then hidden form values are not needed. Instead, any values that you need to pass to $form_state['values'] can be declared in the $form array as such:
    $form['foo'] = array('#type' => 'value', '#value' => 'bar')

    This is accessed in $form_state['values']['foo'], with a value of bar. This method is preferred because the values are not sent to the browser.

  4. To determine where the user should be sent after the form is processed, the _submit function can place a path or URL in $form_state['redirect'] which will be the target of a drupal_goto; every form is redirected after a submit. If you store nothing in $form_state['redirect'], the form will simply be redirected to itself after a submit. It is polite to use drupal_set_message() to explain to the user that the submission was successful.

Understanding the Flow

An important concept with Form API compared to using raw HTML forms (as in Drupal 4.6 and before) is that the drupal_get_form() function handles both presenting and responding to the form. What this means is that the $form array you construct in your function will be built first when the form is presented, and again when the form is submitted.

The practical upshot to this is that many developers immediately find themselves asking the question of "where does my data get stored?". The answer is simply that it doesn't. You put your $form data together, perhaps loading your object from the database and filling in #default_values, the form builder then checks this against what was posted. What you gain from this, however, is that the FormsAPI can deal with your data securely. Faking a POST is much harder since it won't let values that weren't actually on the form come through to the $form_state['values'] in your submit function, and in your 'select' types, it will check to ensure that the value actually existed in the select and reject the form if it was not. In addition, Drupal adds, by default, a security token to each form that will protect against cross-site forgery.

Looking for support? Visit the forums, or join #drupal-support in IRC.


traviss359’s picture

It should be mentioned that in Drupal 6, you must use the hook_theme function to register any theme_form_id functions you are using, according to

TomSherlock’s picture

Note 4 with $form_state['redirect'] probably needs to be unpacked a little more.

I just set $form_state['redirect'] in my module_form_submit
only to find that node_form_submit redirects to
$node_link = l(t('view'), 'node/'. $node->nid)
after node_save, ignoring $form_state['redirect'].

vomitHatSteve’s picture

I've found that the documentation for form redirection is woefully incomplete in general.

Here's what I've figured out in my own experimentation and many hours searching threads here:

1. Setting $form_state['redirect'] within your submit function will redirect the user, but...
2. Setting $form['#redirect'] within the initial form declaration will override #1
3. Calling the form page with a destination $_GET parameter (node/X/edit?destination=...) overrides #1 and #2
4. Sending a destination $_POST variable () overrides all of the above
5. Calling drupal_goto within your validate or submit function will truncate whatever other processing would have been done and redirect the user immediately, ignoring #1-4. (So that one's generally a bad idea.)

I've also heard about people overwriting $_REQUEST['destination'] within their submit function. I believe that would be akin to #3 and #4 above.

msathesh’s picture

To have multiple div elements one can add it like this:

$form['div_tag'][0] = array('#type' => 'markup', '#value' => '
'); $form['div_tag'][1] = array('#type' => 'markup', '#value' => '
'); $form['div_tag'][2] = array('#type' => 'markup', '#value' => '

But if you want the

tags next to the element, for instance, a text field in the form, use the #suffix property to add it next to the text element and #prefix property to add it before the text element for example. It's really helpful in someways.
mitchell-dupe’s picture

I've found that using $form_state['values']['name'] does not work when adding additional validation to a webform. Instead, I've used $form_state['values']['submitted_tree']['name'] to access submitted values and perform custom validation.

baferret’s picture

After 3 hours of work, the only way I've found that works is to use the 'hidden' element in your form builder function. I tried various ways, and I had to use brute force --var_dump($form_state) in my validate function, and found my hidden value.

function myModule_myForm(&$form_state) {
    /* $form['foo'] = array ('#type' => 'value', '#value' => 'bar') ; DOES NOT WORK */

    $form['foo'] = array ('#type' => 'hidden', '#value' => 'bar') ; // WORKS
    return $form ;

// And then in your validate function, use this to access your value:
function myModule_myForm_validate ($form, &$form_state) {
    /* $my_value = $form_state['values']['foo'];  DOES NOT CONTAIN 'bar' */

    $my_value = $form_state['clicked_button']['#post']['foo'] ; // $my_value contains 'bar'
seancr’s picture

"7. Don't forget to register the theme function by implementing hook_theme()"

EDDYL’s picture

I spent 24h to find a way to upload files with 'file element'.

It seems that hook validate and submit needs ($form_id, &$form_values) instead of validate($form_id, &$form_state).

Here is the only working example I found for D6 :

vomitHatSteve’s picture

I think they're using $form_id wrong there.
$form_id is a string ID of the form.
$form (which is actually passed as the first argument to validate and submit) is an array describing the form.
It doesn't really affect the example, but a few months from now when you actually need to access the form_id for some project, you'll be glad to know this tidbit.

I've also usually seen &$form_state used instead of &$form_values, but that's probably just a style opinion.

jhodgdon’s picture

gary4gar reported a correction as an issue:

digitalclover’s picture

I'm trying to set up additional validation for the comment form, yet the conditional isn't recognizing the submitted field value. The field is a custom text field I have included in the comment form. Here's the validation code:

function comment_validation_function($form, &$form_state)
  if ($form_state['values']['field_website'] == 'http://') {
      form_set_error('field_website', t('You must enter a website address.'));

With this code, it always submits the data, even with the default value of "http://". I even tried the afore mentioned


but that leads to an undefined index error. Any guidance at all would be helpful.


mvpfi’s picture

Where this tutorial is giving code after "Let's take a look at a working piece of code using the API", the code won't work before you remove &-character from the second line. Change

function test_myform(&$form_state) {


function test_myform($form_state) {

Another note, to complete the working piece of code, it could be mentioned that the second function should be called for example like this

print render(test_page());

in order to see any output.

tmbritton’s picture

I stared at code I'd adapted from this example before reading down far enough for this comment. Correct code in the sample would have saved me half a day!

miaoulafrite’s picture

the mentionned code (with &) works for D6, where on D7 it should be

function test_myform($form, &$form_state, $other_args_if_you_need){
   // do stuff
B Leg’s picture

How to actually put the form on a page is something that direly needs to be added to this document. There's a ton of information about using hook_menu to get your forms going, but what if it's not supposed to be a menu item?

marcus7777’s picture

Follow the link and I'm lost
Terribly Vague
Posted by seancr on July 18, 2011 at 5:02am
"7. Don't forget to register the theme function by implementing hook_theme()"

skitom1’s picture

It seems that so many of the tutorials on drupal are either missing steps, or do not work if you follow them line for line. It kinda puts you off using this system. I think I will just stick to coding the long way, although longer at least I know what I input will work.... Drupal FORM API hasn't impressed me.