Skip to content

Custom Render Elements

When to Use

When you have complex rendering logic that you reuse across multiple places -- voting widgets, custom table formatters, specialized containers, elements with default behaviors.

Steps: Creating a Render Element Plugin

1. Create the plugin class

namespace Drupal\mymodule\Plugin\Element;

use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element\RenderElementBase;

/**
 * Provides an alert box render element.
 *
 * Properties:
 * - #alert_type: Alert type (success, warning, error, info). Default: 'info'.
 * - #message: Alert message text.
 * - #dismissible: Whether alert can be dismissed. Default: FALSE.
 *
 * Usage:
 * @code
 * $build['alert'] = [
 *   '#type' => 'alert_box',
 *   '#alert_type' => 'success',
 *   '#message' => $this->t('Operation completed successfully.'),
 *   '#dismissible' => TRUE,
 * ];
 * @endcode
 */
#[RenderElement('alert_box')]
class AlertBox extends RenderElementBase {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    return [
      '#alert_type' => 'info',
      '#message' => '',
      '#dismissible' => FALSE,
      '#pre_render' => [
        [static::class, 'preRenderAlertBox'],
      ],
      '#theme_wrappers' => ['container'],
    ];
  }

  /**
   * Pre-render callback: Build the alert box markup.
   */
  public static function preRenderAlertBox($element) {
    $type = $element['#alert_type'];
    $dismissible_class = $element['#dismissible'] ? ' alert-dismissible' : '';

    $element['#attributes']['class'][] = 'alert';
    $element['#attributes']['class'][] = 'alert-' . $type . $dismissible_class;
    $element['#attributes']['role'] = 'alert';

    $element['message'] = [
      '#markup' => $element['#message'],
    ];

    if ($element['#dismissible']) {
      $element['close'] = [
        '#type' => 'html_tag',
        '#tag' => 'button',
        '#value' => 'x',
        '#attributes' => [
          'type' => 'button',
          'class' => ['close'],
          'data-dismiss' => 'alert',
          'aria-label' => t('Close'),
        ],
      ];
      $element['#attached']['library'][] = 'mymodule/alert-dismissible';
    }

    return $element;
  }

}

2. Clear cache

drush cr

3. Use the element

$build['success_message'] = [
  '#type' => 'alert_box',
  '#alert_type' => 'success',
  '#message' => $this->t('Your changes have been saved.'),
  '#dismissible' => TRUE,
];

Reference: core/lib/Drupal/Core/Render/Element/Link.php -- real-world example

Pattern: Form Element Plugin

For elements that work inside forms:

namespace Drupal\mymodule\Plugin\Element;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Attribute\FormElement;
use Drupal\Core\Render\Element\FormElementBase;

#[FormElement('color_picker')]
class ColorPicker extends FormElementBase {

  public function getInfo() {
    return [
      '#input' => TRUE,
      '#default_value' => '#000000',
      '#process' => [[static::class, 'processColorPicker']],
      '#element_validate' => [[static::class, 'validateColorPicker']],
      '#theme_wrappers' => ['form_element'],
    ];
  }

  public static function processColorPicker(&$element, FormStateInterface $form_state, &$complete_form) {
    $element['#attributes']['type'] = 'color';
    $element['#attributes']['class'][] = 'color-picker';
    $element['#attached']['library'][] = 'mymodule/color-picker';

    return $element;
  }

  public static function validateColorPicker(&$element, FormStateInterface $form_state, &$complete_form) {
    $value = $element['#value'];
    if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $value)) {
      $form_state->setError($element, t('Invalid color format. Use #RRGGBB.'));
    }
  }

}

Reference: core/lib/Drupal/Core/Render/Element/FormElementBase.php

Decision: Render Element vs Theme Hook

Use Render Element Plugin When... Use Theme Hook When...
Logic is complex and reusable Presentation is primary concern
Default properties/behaviors needed Simple variable -> template mapping
Element needs pre-render processing No special processing needed
Want plugin alterability Theme-level alterability sufficient

Common Mistakes

  • Not clearing cache after creating plugin -- Plugin not discovered
  • Forgetting #[RenderElement('element_type')] attribute -- Plugin not registered
  • Not extending RenderElementBase or FormElementBase -- Missing required methods
  • Using underscores in element type name -- Convention is hyphens or no separator
  • Not documenting properties in class docblock -- Users don't know what's available
  • Overcomplicating simple theming needs -- Theme hook might be simpler

See Also