Brian Wagner | Blog

Drupal Render Array: What Is It and Why Should I Care?

May 23, 2017 | Last edit: Dec 31, 2020

Any foray into Drupal frontend development runs headlong into the term 'render array' at some point. Maybe it's self-explanatory to someone, somewhere, but not to most mortals like me.

As with most complicated things, you can read the technical description, or several, and come away feeling like you understand things less. That's frustrating. Personally I've found it more enlightening to jump into examples and swim around in the deep end before things start to make sense. 

But why should we jump in the water at all? Maybe you don't feel like swimming today. You don't like pools. Why should you want to spend precious time learning about Drupal's render arrays?

Why Should You Care?

Drupal's render process is something of a black box. I don't understand how it all works. Some things go in to the render process, things happen, and we get formatted HTML at the end. Few people care how the sausage is made. All that matters is that we start with beef and get a hot dog in the end.

With Drupal, we are almost always dealing with formatted HTML. So to get that end product, it can help to understand what goes into the recipe, if only to be able to add a dash of salt here or cut back the garlic there.

Have you ever wanted to:

  • add a class to a custom element?
  • tweak a custom form?
  • debug a third-party module to get the right elements on the page?
  • make a custom link open in a new window?

Chance are you know how to do this with plain html, and trying to make Drupal do it is tearing you hair out. This is when it's valuable to understand how render arrays work. And to that end, it's only worth knowing as much as you need to solve the problem in front of you.

Caveat: if you need to solve those problems above through the Drupal UI, then save your time and go read something else. There are other ways to resolve those issues.

Render Array Basics

A render array is an array. It goes through the Drupal render pipeline. It generates HTML elements. A basic site page is a multilevel array: the page array contains the node array which contains the title array which contains ... The # hash # is a big giveaway that you're looking at render elements.

In my experience, there are three types of render arrays that are important to distinguish.

- #markup: generates the most basic of text on a page, typically.

$element = array(
  '#markup' => 'Text to display',
);

- #type => html_tag: used with an HTML tag, this is handy to specify the HTML element you want, without requiring a separate template file.

$element = array(
  '#type' => 'html_tag',
  '#tag' => 'h4',
  '#value' => 'Text to display',
  '#attributes' => array(
    'class' => array('header-class', 'important-thing'),
  ),
);

- #theme: calls a specific Drupal theming pattern, i.e. images with image styles. You can also create a custom theme pattern and call it here.

 $element = array(
  '#theme' => 'image_style',
  '#width' => $variables['width'],
  '#height' => $variables['height'],
  '#style_name' => $variables['style_name'],
  '#uri' => $variables['uri'],
);

Source

Hyperlinks have moved away from the render array format in Drupal 8 to more of the black-box method. We have special class methods to generate links, Link::fromTextAndUrl(text, Url), which are a separate topic.

Simple Examples

Let's say you have a module that fetches data from an external API or somewhere else outside the normal Drupal data structure. Now we can use that data and build the content for a custom block.

The data will have a title and two items; each item has an id and name property.

The block will have a div to hold it all; an H3 element for a title, and individual P items. Also we're adding attributes.

  $element['api-data'] = [
    '#type' => 'container',
    '#attributes' => [
      'class' => ['api-data']
    ]
  ];
 
  $element['api-data']['title'] = [
    '#type' => 'html_tag',
    '#tag' => 'h2',
    '#value' => $api_data['title'],
  ];
 
  foreach($api_data['items'] as $item) {
    $element['api-data'][$item['id']] = [
      '#type' => 'html_tag',
      '#tag' => 'p',
      '#value' => $item['name'],
      '#attributes' => [
        'id' => $item['id'],
        'onclick' => 'clickOnAPIItem(this)',
      ]
    ];
  }

Gist

Final output:

HTML markup

One of the first things to notice is that this is a nested array. That's how the Drupal render process is made to work. It descends through the entire tree and renders everything, maintaining that structure in the HTML output.

Another thing is we don't have to call render() at any point. Drupal will do that for us when it's ready. If we have more complicated logic, we can pass around data and the render array from one function to another, adding things or editing things. It's much easier to do that when the data is an array, rather than an HTML element that we have to construct and deconstruct along the way.

Other things: We can generate items in for-loop. We can add 'class' and 'id' and 'onclick' properties, among others.

If this is overwhelming at first, that's ok. All of these pieces work together or independently. Seeing all of this here can help us when modifying elements as well. A common question is, "how do I add a class to an element?" 

Threads on drupal.org or StackExchange will explain how to target the specific element. That's a separate topic. But then we get to the code that modifies the element and see this:

$variables['attributes']['class'][] = 'page-node-' . $node->id();

From above, we know that 'class' properties live inside the 'attributes' array. And 'class' is an array, so we can just add another item in there. 

Drupal 7 vs. Drupal 8

While many things have changed from 7 to 8, the basics of render arrays remain the same.

One interesting addition is the use of 'inline_template'. It allows us to declare a (small?) template with variables, and then pass the variables in the array as well. Example.

$content['items'] = [
  '#type' => 'inline_template',
  '#template' => '{% for item in row %} {{ item.name }} {% endfor %}',
  '#context' => [
    'row' => [
      ['name' => 'first name'],
      ['name' => 'second name'],
    ],
  ],
];