Render Arrays Basics - How Drupal Builds Pages?

Welcome to one of the most fundamental concepts in Drupal development: Render Arrays. If you've completed the previous foundation topics, you've learned about modules, themes, entities, and hooks. Now it's time to understand how Drupal actually builds and displays content on the page.

Think of render arrays as Drupal's recipe for creating HTML. Instead of writing HTML directly, you create a structured PHP array that tells Drupal what to display and how to display it. This approach gives you incredible flexibility and power.

What is a Render Array?

A render array is a structured PHP associative array that describes how content should be rendered into HTML. It's Drupal's way of separating the data (what to show) from the presentation (how to show it).

Why Not Just Write HTML?

You might wonder: "Why not just write HTML directly?" Here's why render arrays are better:

  1. Theming flexibility - Themes can alter how content displays without touching your module code
  2. Caching - Drupal can cache parts of the page automatically
  3. Alterability - Other modules can modify your output using hooks
  4. Security - Automatic XSS protection and sanitization
  5. Lazy rendering - Content is only rendered when needed

Basic Structure

Every render array is a PHP array with special keys that start with #. These are called render array properties.

Your First Render Array

$build = [
  '#markup' => '<p>Hello, Drupal!</p>',
];

This is the simplest render array possible. The #markup property tells Drupal to output this HTML directly.

Common Render Array Properties

Here are the most important properties you'll use:

PropertyPurposeExample
#markupRaw HTML output'#markup' => '<p>Text</p>'
#plain_textPlain text (auto-escaped)'#plain_text' => 'User input'
#typeRender element type'#type' => 'container'
#themeTemplate to use'#theme' => 'item_list'
#cacheCaching metadata'#cache' => ['tags' => ['node:1']]
#attachedCSS/JS libraries'#attached' => ['library' => ['mymodule/mylib']]

Building Content with Render Arrays

Example 1: Simple Text Output

/**
 * Implements hook_page_top().
 */
function mymodule_page_top(array &$page_top) {
  $page_top['mymodule_message'] = [
    '#markup' => '<div class="welcome-message">Welcome to our site!</div>',
  ];
}

Example 2: Safe User Input

When displaying user-provided content, NEVER use #markup - use #plain_text instead:

// WRONG - Vulnerable to XSS attacks
$build = [
  '#markup' => $user_input,
];

// CORRECT - Automatically escaped
$build = [
  '#plain_text' => $user_input,
];

Example 3: Using Render Element Types

Drupal provides pre-built render element types:

$build = [
  '#type' => 'container',
  '#attributes' => [
    'class' => ['my-wrapper'],
    'id' => 'custom-container',
  ],
  'content' => [
    '#markup' => '<p>Content inside container</p>',
  ],
];

Common render element types:

  • container - Simple div wrapper
  • html_tag - Any HTML tag
  • link - Generates a link
  • details - Collapsible fieldset
  • table - Data table

Nested Render Arrays

Render arrays can contain other render arrays, creating a tree structure:

$build = [
  '#type' => 'container',
  '#attributes' => ['class' => ['article-wrapper']],
  
  'header' => [
    '#type' => 'html_tag',
    '#tag' => 'h2',
    '#value' => 'Article Title',
  ],
  
  'content' => [
    '#type' => 'container',
    'paragraph1' => [
      '#markup' => '<p>First paragraph.</p>',
    ],
    'paragraph2' => [
      '#markup' => '<p>Second paragraph.</p>',
    ],
  ],
  
  'footer' => [
    '#markup' => '<div class="footer">Published on Jan 17, 2026</div>',
  ],
];

Practical Example: Custom Block

Let's create a custom block that displays recent articles using render arrays:

<?php

namespace Drupal\mymodule\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'Recent Articles' Block.
 *
 * @Block(
 *   id = "recent_articles_block",
 *   admin_label = @Translation("Recent Articles"),
 * )
 */
class RecentArticlesBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    // Start with a container
    $build = [
      '#type' => 'container',
      '#attributes' => ['class' => ['recent-articles']],
    ];

    // Add a heading
    $build['heading'] = [
      '#type' => 'html_tag',
      '#tag' => 'h3',
      '#value' => $this->t('Recent Articles'),
    ];

    // Create a list of articles
    $articles = [
      ['title' => 'Understanding Render Arrays', 'date' => '2026-01-17'],
      ['title' => 'Drupal Hooks Explained', 'date' => '2026-01-16'],
      ['title' => 'Service Container Deep Dive', 'date' => '2026-01-15'],
    ];

    $items = [];
    foreach ($articles as $article) {
      $items[] = [
        '#markup' => '<strong>' . $article['title'] . '</strong> - ' . $article['date'],
      ];
    }

    // Use the item_list theme
    $build['articles'] = [
      '#theme' => 'item_list',
      '#items' => $items,
      '#list_type' => 'ul',
      '#attributes' => ['class' => ['article-list']],
    ];

    return $build;
  }
}

Using Theme Functions

Instead of writing HTML in #markup, you can use Drupal's theme system:

$build = [
  '#theme' => 'item_list',
  '#items' => [
    'First item',
    'Second item',
    'Third item',
  ],
  '#title' => 'My List',
  '#list_type' => 'ol',
];

This uses the item-list.html.twig template and is much more flexible.

Attaching CSS and JavaScript

Add libraries to your render array:

$build = [
  '#markup' => '<div class="custom-widget">My Widget</div>',
  '#attached' => [
    'library' => [
      'mymodule/widget-styling',
    ],
  ],
];

First, define the library in mymodule.libraries.yml:

widget-styling:
  css:
    theme:
      css/widget.css: {}
  js:
    js/widget.js: {}
  dependencies:
    - core/jquery

Caching Render Arrays

One of the most powerful features of render arrays is built-in caching:

$build = [
  '#markup' => '<p>This content is cached for 1 hour</p>',
  '#cache' => [
    'max-age' => 3600, // Cache for 1 hour
    'contexts' => ['user'], // Different cache per user
    'tags' => ['node:5'], // Invalidate when node 5 changes
  ],
];

Cache Contexts

Cache contexts make the cache vary by certain conditions:

  • user - Different cache per user
  • url - Different cache per URL
  • languages - Different cache per language
  • theme - Different cache per theme

Cache Tags

Cache tags allow automatic invalidation:

'#cache' => [
  'tags' => [
    'node:5',           // Invalidate when node 5 changes
    'user:3',           // Invalidate when user 3 changes
    'config:system.site', // Invalidate when site config changes
  ],
],

Render Array in a Controller

Here's a complete example of using render arrays in a controller:

<?php

namespace Drupal\mymodule\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Returns responses for My Module routes.
 */
class MyModuleController extends ControllerBase {

  /**
   * Builds the response.
   */
  public function content() {
    $build = [];

    // Page title
    $build['title'] = [
      '#type' => 'html_tag',
      '#tag' => 'h1',
      '#value' => $this->t('Welcome to My Module'),
    ];

    // Introduction paragraph
    $build['intro'] = [
      '#type' => 'html_tag',
      '#tag' => 'p',
      '#value' => $this->t('This page demonstrates render arrays.'),
      '#attributes' => ['class' => ['intro-text']],
    ];

    // A themed table
    $build['data_table'] = [
      '#type' => 'table',
      '#header' => [
        $this->t('Name'),
        $this->t('Value'),
      ],
      '#rows' => [
        ['Setting 1', 'Value 1'],
        ['Setting 2', 'Value 2'],
        ['Setting 3', 'Value 3'],
      ],
      '#attributes' => ['class' => ['data-table']],
    ];

    // Attach CSS
    $build['#attached']['library'][] = 'mymodule/mymodule-styling';

    // Cache for 5 minutes
    $build['#cache'] = [
      'max-age' => 300,
    ];

    return $build;
  }
}

Common Render Array Patterns

Pattern 1: Link Element

$build['my_link'] = [
  '#type' => 'link',
  '#title' => $this->t('Visit Drupal.org'),
  '#url' => \Drupal\Core\Url::fromUri('https://drupal.org'),
  '#attributes' => [
    'class' => ['external-link'],
    'target' => '_blank',
  ],
];

Pattern 2: Details Element (Collapsible)

$build['details'] = [
  '#type' => 'details',
  '#title' => $this->t('Advanced Options'),
  '#open' => FALSE,
  'content' => [
    '#markup' => '<p>Hidden content goes here</p>',
  ],
];

Pattern 3: Inline Template

$build['custom'] = [
  '#type' => 'inline_template',
  '#template' => '<div class="{{ class }}">{{ message }}</div>',
  '#context' => [
    'class' => 'custom-message',
    'message' => $this->t('Hello from inline template'),
  ],
];

Debugging Render Arrays

Use Drupal's built-in debugging tools:

1. Kint (with Devel module)

// Install devel module, then:
kint($build);

2. Drupal Debug Function

\Drupal::messenger()->addMessage('<pre>' . print_r($build, TRUE) . '</pre>');

3. Browser DevTools

Enable Twig debugging in sites/default/services.yml:

parameters:
  twig.config:
    debug: true

This shows you which templates are being used in HTML comments.

Best Practices

1. Always Use Proper Keys

// GOOD - Uses string keys
$build['header'] = [...];
$build['content'] = [...];

// BAD - Numeric keys are less maintainable
$build[0] = [...];
$build[1] = [...];

2. Separate Data from Presentation

// GOOD - Uses theme function
$build = [
  '#theme' => 'mymodule_widget',
  '#data' => $data,
];

// BAD - HTML in code
$build = [
  '#markup' => '<div class="widget">' . $data . '</div>',
];

3. Use #plain_text for User Input

// GOOD - Secure
$build['name'] = [
  '#plain_text' => $user_name,
];

// BAD - XSS vulnerability
$build['name'] = [
  '#markup' => $user_name,
];

4. Add Cache Metadata

// GOOD - Properly cached
$build = [
  '#markup' => $content,
  '#cache' => [
    'tags' => ['node:' . $node->id()],
    'contexts' => ['user'],
  ],
];

// BAD - No cache metadata
$build = [
  '#markup' => $content,
];

Common Mistakes to Avoid

Mistake 1: Forgetting the # Symbol

// WRONG
$build = [
  'markup' => '<p>Text</p>',
];

// CORRECT
$build = [
  '#markup' => '<p>Text</p>',
];

Mistake 2: Using #markup for User Input

// WRONG - Security risk!
$build = [
  '#markup' => $_GET['user_input'],
];

// CORRECT
$build = [
  '#plain_text' => $_GET['user_input'],
];

Mistake 3: Returning Wrong Structure from Controller

// WRONG - String instead of array
public function content() {
  return '<p>Hello</p>';
}

// CORRECT - Render array
public function content() {
  return [
    '#markup' => '<p>Hello</p>',
  ];
}

Summary

Render arrays are the foundation of how Drupal builds pages. Remember:

  • Render arrays are structured PHP arrays with # prefixed keys
  • They separate data from presentation
  • Use #plain_text for user input, never #markup
  • Nest arrays to build complex structures
  • Add caching metadata to improve performance
  • Attach CSS/JS libraries through #attached
  • Use theme functions instead of raw HTML when possible

Additional Resources


Practice makes perfect! 
Try building different combinations of render arrays in your development environment. Experiment with the properties, try different render element types, and see how they all work together.

Happy coding!