Render API

In Drupal, pages, blocks and forms are built as PHP data first and turned into HTML last, so themes can change the markup and caches can reuse it.

Learn DrupalDrupal glossary

The Render API is the part of Drupal core that turns structured data into HTML. Modules return render arrays, nested PHP arrays whose properties such as #type, #theme, #markup and #cache say what to show and how, instead of writing markup. The renderer walks the array, calls the theme system and applies caching.

How the Render API works in Drupal 11

In a render array, keys that start with # are properties and all other keys are children, which are render arrays themselves. Core documents the design goal: module code returns structured data so a theme can control the markup.

The main properties are:

  • #type picks a render element. Elements are plugins; the textfield, for example, is a class with the #[FormElement('textfield')] attribute in the Element plugin namespace.
  • #theme picks a theme hook, defined with hook_theme() and implemented by a template, normally Twig.
  • #markup and #plain_text hold simple output. #markup is filtered with Xss::filterAdmin(), which removes script and style tags.
  • #cache holds keys, contexts, tags and max-age. Contexts, tags and max-age bubble up to the parent arrays.
  • #attached adds libraries, drupalSettings and other page-level data.
  • #pre_render, #post_render and #lazy_builder hold callbacks.

In 11.2, Renderer::doRender() handles one element in this order: it checks #access, returns a cached copy if #cache has keys and a copy exists, handles a #lazy_builder if there is one, either at once or through a placeholder, then runs the #pre_render callbacks. It filters #markup, renders the children (through #theme or by recursion), applies #theme_wrappers, runs #post_render callbacks and adds #prefix and #suffix.

Placeholdering keeps pages cacheable. By default, renderer.config treats a fragment with max-age 0, or one that varies by session or user, as a candidate for a placeholder that is rendered last.

Example

A controller method returns a render array, and Drupal does the rest:

$build['intro'] = [
          '#markup' => '<p>Welcome back.</p>',
        ];
        $build['links'] = [
          '#theme' => 'item_list',
          '#items' => ['First', 'Second'],
          '#cache' => [
            'tags' => $node->getCacheTags(),
            'contexts' => ['languages'],
          ],
        ];
        $build['#attached']['library'][] = 'my_module/slider';
        return $build;

The cache tags make Drupal drop the cached list when the node changes. The my_module/slider library is a placeholder name for an asset library the module would define.

Common mistakes

  • Calling render() outside a render context. Drupal throws a LogicException that tells you to use renderRoot(), renderInIsolation() or a #lazy_builder or #pre_render callback instead. The older renderPlain() is deprecated since Drupal 10.3.
  • Untrusted callbacks. Render callbacks must be anonymous functions or methods of a class that implements TrustedCallbackInterface or RenderCallbackInterface, a rule introduced in Drupal 8.8.0. In Drupal 11 any other callback throws an UntrustedCallbackException.
  • Expecting script tags in #markup. The admin tag filter removes them. Attach a library, or use a theme hook.
  • Missing cache metadata. If an array depends on data that can change or on the user's language or role, declare the tags and contexts. The metadata is what tells Drupal when to vary or invalidate the cached copy.
  • Complex arguments to a lazy builder. Drupal's assertions require scalar values or NULL in the argument list, and a lazy builder element cannot have children.

For marketing and content teams

You rarely see render arrays, but you see their effects. Because modules hand structured data to the theme, one change in the theme can restyle a list or a form everywhere it appears. Because the output is cached with tags, Drupal knows which cached pieces to drop when you save a node. If a change does not appear on a page, ask the developer whether the component declares cache tags for the data it displays.

Sources

Questions about Render API

#markup holds HTML, which Drupal filters with the admin tag list from Xss; #allowed_tags changes that list. #plain_text holds text that Drupal escapes instead, and it takes precedence over #markup.

#type names a render element, a plugin that supplies default properties for things like a textfield or a table. #theme names a theme hook, which a module defines with hook_theme() and implements with a template, normally in Twig.

A #lazy_builder property holds a callback and a list of scalar arguments. Drupal can postpone the callback until the rest of the page is rendered, so a personalized fragment does not make the whole page uncacheable.

Related terms

All Drupal terms from A to Z