CMS Architecture Reference / Drupal 6 to Drupal 11

Drupal Login Form Theming: Legacy Concepts and Modern Techniques

A comprehensive engineering reference tracing Drupal user login form customisation from the classic April 2009 Aphex Creations Drupal 6 PHPTemplate tutorial to modern Drupal 10 and 11 Twig templates, Render API hooks, WCAG 2.2 accessibility, and authentication security.

Historical Source
Aphex Creations Blog (Apr 2009)
Original Author
Brendon Crawford
Legacy Scope
Drupal 6 / 7 PHPTemplate
Modern Scope
Drupal 10 / 11 Twig & Form API
01 / Historical Provenance / April 2009

Historical Article Attribution

In April 2009, software engineer Brendon Crawford published “Theming the Drupal 6 User Login Form” at blog.aphexcreations.net/2009/04/theming-drupal-user-login-form.html as part of the Aphex Creations technical blog. Alongside maintaining the official Drupal ajax project module (drupal.org/project/ajax) and the AJFORM AJAX JavaScript Toolkit, Crawford documented a clean, repeatable method for registering custom PHPTemplate overrides for Drupal’s user_login and user_login_block forms.

Cited by international Drupal communities—including Drupal Italia (drupalitalia.org) and developer knowledge bases across Japan and Europe—the article solved one of the most common pitfalls in Drupal theming: customising the visual markup of the login fields without breaking Drupal’s internal Form API validation tokens.

02 / Drupal 6 & Drupal 7 Architecture

Legacy Drupal Login Form Theming (PHPTemplate)

In Drupal 6 and Drupal 7, forms were structured as nested associative PHP arrays rendered by the PHPTemplate engine. By default, Drupal rendered the entire user_login form through generic form element wrappers. To customise the layout in a theme’s template.php file, developers had to register the form ID in hook_theme(), extract individual fields in a preprocess function, and always call drupal_render() (Drupal 6) or drupal_render_children() (Drupal 7) on the remaining form array.

Legacy Drupal 6: Registering & Preprocessing user_login in template.php (php)
<?php
/**
 * Historical Drupal 6 pattern (template.php)
 * Registers user-login.tpl.php for the user_login form ID.
 */
function mytheme_theme($existing, $type, $theme, $path) {
  return array(
    'user_login' => array(
      'arguments' => array('form' => NULL),
      'template' => 'user-login',
    ),
    'user_login_block' => array(
      'arguments' => array('form' => NULL),
      'template' => 'user-login-block',
    ),
  );
}

function mytheme_preprocess_user_login(&$variables) {
  $variables['intro_text'] = t('Sign in to access your developer account.');
  // Render individual visible controls into template variables
  $variables['name_field'] = drupal_render($variables['form']['name']);
  $variables['pass_field'] = drupal_render($variables['form']['pass']);
  $variables['submit_button'] = drupal_render($variables['form']['submit']);
  // CRITICAL: Render remaining hidden elements (form_build_id, form_id, form_token)
  $variables['rendered_hidden'] = drupal_render($variables['form']);
}
Legacy Drupal 6: user-login.tpl.php Template Markup (php)
<div class="custom-user-login-wrapper">
  <p class="login-intro"><?php print $intro_text; ?></p>
  <div class="login-field-group">
    <?php print $name_field; ?>
  </div>
  <div class="login-field-group">
    <?php print $pass_field; ?>
  </div>
  <div class="login-actions">
    <?php print $submit_button; ?>
  </div>
  <?php
    // Omitting this line causes Drupal Form API submission to fail silently!
    print $rendered_hidden;
  ?>
</div>

The most frequent bug encountered by Drupal 6/7 developers was printing $form['name'], $form['pass'], and $form['submit'] individually while forgetting to print the remainder of $form. Without form_build_id and form_id in the submitted POST payload, Drupal’s Form API rejected the login attempt altogether.

03 / Drupal 10 & Drupal 11 Architecture

Modern Drupal Login Form Theming with Twig & Form Alter

Beginning with Drupal 8 and continuing through modern Drupal 10 and Drupal 11, PHPTemplate and template.php were replaced by the Twig templating engine and *.theme files. Modifying the login form cleanly separates structural logic (in hook_form_user_login_form_alter()) from presentation markup (in form--user-login-form.html.twig).

Modern Drupal 10/11: mytheme.theme (Form Alter & Theme Suggestion) (php)
<?php

declare(strict_types=1);

use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_FORM_ID_alter() for user_login_form.
 */
function mytheme_form_user_login_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
  $form['name']['#attributes']['autocomplete'] = 'username';
  $form['name']['#attributes']['placeholder'] = t('Username or email');

  $form['pass']['#attributes']['autocomplete'] = 'current-password';
  $form['pass']['#attributes']['placeholder'] = t('Password');

  $form['actions']['submit']['#attributes']['class'][] = 'btn-primary-auth';
}

/**
 * Implements hook_theme_suggestions_form_alter().
 */
function mytheme_theme_suggestions_form_alter(array &$suggestions, array $variables): void {
  if (!empty($variables['element']['#form_id'])) {
    $suggestions[] = 'form__' . $variables['element']['#form_id'];
  }
}
Modern Drupal 10/11: templates/form/form--user-login-form.html.twig (twig)
{#
  Custom Twig template for the Drupal user login form.
  Always output {{ element|without('name', 'pass', 'actions') }} so hidden
  Form API tokens (form_build_id, form_id, form_token) remain intact.
#}
<form{{ attributes.addClass('auth-login-form') }}>
  <div class="auth-login-form__fields">
    {{ element.name }}
    {{ element.pass }}
  </div>

  <div class="auth-login-form__actions">
    {{ element.actions }}
  </div>

  {{ element|without('name', 'pass', 'actions') }}
</form>

Notice how Twig’s |without(...) filter solves the exact same problem that drupal_render($variables['form']) solved in 2009: it renders every remaining child element (including form_build_id, form_id, and any contributed CAPTCHA or Two-Factor Authentication elements) without duplicating the username, password, or submit controls.

04 / WCAG 2.2 AA Compliance

Accessibility Best Practices for Login Forms

Customising authentication templates carries high accessibility responsibility. Under WCAG 2.2 Success Criterion 3.3.8 (Accessible Authentication) and core form usability standards, themed login forms must preserve:

  • Persistent Visible Labels: Never hide <label> elements in favour of placeholder-only inputs. Placeholders disappear on input and fail screen-reader and cognitive accessibility guidelines.
  • Password Manager & Paste Support: Never block clipboard paste events on password inputs, and always attach explicit autocomplete="username" and autocomplete="current-password" attributes so browser credential managers and passkeys work reliably.
  • Programmatic Error Association: When authentication fails, error messages must be linked to the input via aria-describedby and marked with aria-invalid="true" so assistive technologies announce the failure immediately.
05 / Architectural Evolution

Drupal Version Compatibility Reference

Drupal VersionForm IDTemplate EngineRemaining Hidden Fields Helper
Drupal 6 (Legacy EOL)user_loginPHPTemplate (user-login.tpl.php)drupal_render($form)
Drupal 7 (Legacy EOL)user_loginPHPTemplate (user-login.tpl.php)drupal_render_children($form)
Drupal 8 / 9 (EOL)user_login_formTwig 1 / 2 (form--user-login-form.html.twig){{ element|without(...) }}
Drupal 10 / 11 (Current)user_login_formTwig 3 (form--user-login-form.html.twig){{ element|without(...) }}
06 / Authentication Hardening

Authentication Security & Form API Integrity

When theming or AJAX-enabling a Drupal login form, never bypass Drupal’s built-in Form API validation lifecycle:

  • Hidden Form API Tokens: Always preserve form_build_id and form_id. Stripping these in a custom Twig or PHPTemplate file breaks CSRF and form state caching.
  • Flood Control Protection: Drupal’s core user.flood service rate-limits failed login attempts per IP address and per username. Custom login controllers must never authenticate credentials directly against the database without invoking flood checks.
  • Username Enumeration Prevention: Ensure validation messages on login and password-reset forms do not disclose whether a specific email address or username exists in the database.

For additional historical PHP and JavaScript references preserved from the Aphex Creations archive, explore our Aphex Creations Historical Archive, the AJFORM AJAX JavaScript Toolkit Guide, and our interactive JavaScript Regex Tester.

Connected Reference & Utilities