How to register layouts

Last updated on
5 August 2022

The Layout API was included (originally experimental) in Drupal 8.3.x core. 

There are several ways to provide a layout. We'll discuss each in the sections below, starting with the simplest, most common case and building up to some of the more advanced techniques!

Registering layouts using *.layouts.yml

The most basic way to register layouts, is to put a *.layouts.yml file in your module or theme.

The first part of the file name is the machine name of your module or theme, so if for example, your module's machine name is my_custom_module, you'd call the file my_custom_module.layouts.yml.

This file should be placed in the top-level directory of your module or theme, and you'll need to rebuild the cache (for example, with Drush it's drush cr) for your changes to the file to be picked up.

In the next few sections, we'll cover the different keys and values you can use in this file.

The simplest case

This is the simplest, and easiest way to register a new layout! For most use cases, you shouldn't need to read beyond this section.

Here's an example of a super simple *.layouts.yml:

one_column:
  label: 'One column'
  category: 'My Layouts'
  template: templates/one-column
  default_region: main
  regions:
    main:
      label: Main content
two_column:
  label: 'Two column'
  category: 'My Layouts'
  template: templates/two-column
  default_region: main
  regions:
    main:
      label: Main content
    sidebar:
      label: Sidebar

This registers two layouts: "One column" and "Two column". The 'label' and 'category' keys are required.

You can see that the first layout declares a single "Main content" region, and the second has both a "Main content" and "Sidebar" region.

At minimum, each layout should specify a Twig template. The 'template' should not include the .html.twig extension on the file, which means if you specified templates/one-column the actual file will be templates/one-column.html.twig. Make sure the value of 'template' is not an already existing theme hook.

In your Twig template, you can use tokens like {{ content.main }} and {{ content.sidebar }} to output the content of each region. So, for example, templates/two-column.html.twig could look like:

<div class="two-column">
  <div class="main-region">
    {{ content.main }}
  </div>
  <div class="sidebar-region">
    {{ content.sidebar }}
  </div>
</div>

Notes & Considerations

  • Regions cannot have a dash ('-') in their names. 
  • You DON'T need to write your own hook_theme(), the Layout API takes care of this for you.

Creating a layout with drag-and-drop regions

For your custom layout to allow for drag-and-drop ability, you need to add some attributes to your twig template. Here is a more advanced version of the templates/two-column.html.twig  template above:

{% if content %}
  <div {{ attributes.addClass('two-column') }}>
  {% if content.main %}
    <div {{ region_attributes.main.addClass('main-region') }}>
      {{ content.main }}
    </div>
  {% endif %}
  {% if content.sidebar %}
    <div {{ region_attributes.sidebar.addClass('sidebar-region') }}>
      {{ content.sidebar }}
    </div>
  {% endif %}
  </div>
{% endif %}

Note that the block elements have to be direct children of their region for drag and drop to function.

Registering your own template and using 'theme'

In the first section, we specified the 'template' key and it automatically registered it with the theme system.

It's also possible to specify a particular theme hook (which you've manually registered using hook_theme()). This can be useful if, for example, you want to use the same template to render multiple layouts.

To do this, you need to first register your template using hook_theme(). For example, if wanted to declare a theme hook called "advanced_layout_1" you put this in your *.module (if a module) or *.theme (if a theme) file:

// Replace "MY_MODULE_OR_THEME" with the machine name of your module or theme.
function MY_MODULE_OR_THEME_theme() {
  return [
    'advanced_layout_1' => [
      'template' => 'templates/advanced-layout-1',
      // layout_plugin expects the theme hook to be declared with this:
      'render element' => 'content',
      'base hook' => 'layout',
    ],
  ];
}

Then in your *.layouts.yml file, don't use 'template', but specify the machine name of your theme hook in 'theme_hook'.

For example, if your theme hook is "advanced_layout_1", your *.layouts.yml could look like:

advanced_layout_1:
  label: Advanced Layout 1
  category: My Layouts
  theme_hook: advanced_layout_1
  regions:
    main:
      label: Main content

(Note: You can also give a specific theme suggestion in 'theme_hook', like "advanced_layout_1__alternate_suggestion", by putting the suggestion after a double underscore "__".)

Registering your own library and using 'library'

Most layout templates have associated CSS that must be loaded.

To do this, you need to first register your library with the asset management system using *.libraries.yml. Here's a simple example:

advanced-layout-library:
  version: 1.x
  css:
    theme:
      css/advanced-layout-library.css: {}
  js:
    js/advanced-layout-library.js: {}
  dependencies:
    - core/jquery

Then in your *.layouts.yml file, specify the machine name of your library in 'library'.

For example, if your library is "advanced-layout-library", your *.layouts.yml could look like:

advanced_layout_2:
  label: Advanced Layout 2
  category: My Layouts
  template: templates/advanced-layout-2
  # Replace "MY_MODULE_OR_THEME" with the machine name of your module or theme.
  library: MY_MODULE_OR_THEME/advanced-layout-library
  regions:
    main:
      label: Main content

Automatically rendering icon used in the layout builder interface

You can also specify icon_map: This is used to describe the icon used when the site builder is choosing a layout in the layout builder interface, these icons are built as SVG automatically and described in your layouts YML.

For rendering columns that span through multiple rows, just repeat the region name:

icon_map:
  - [square_one, square_two, square_three]
  - [rectangle_vertical, rectangle_horizontal, rectangle_horizontal]
  - [rectangle_vertical, square_four, square_five]

The above code snippet produces the following icon map: 

Using an alternate 'class'

So far, in the previous examples we haven't had to write much (or any) PHP code! This works by using the default layout class (\Drupal\Core\Layout\LayoutDefault) for any layouts that don't specify a 'class' key.

However, it's possible to use a custom 'class' for a layout! This can be useful if, for example, you want your layout to have custom settings so that users can change the way the layout is rendered.

(Note: You can specify a custom 'class' with layouts registered in both modules and themes, BUT the class itself MUST be in a module. Themes can't contain autoloadable class files.)

Here's an example layout class which provides a settings form:

namespace Drupal\my_custom_module;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Plugin\PluginFormInterface;

class MyLayoutClass extends LayoutDefault implements PluginFormInterface {

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return parent::defaultConfiguration() + [
      'extra_classes' => 'Default',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $configuration = $this->getConfiguration();
    $form['extra_classes'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Extra classes'),
      '#default_value' => $configuration['extra_classes'],
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
    // any additional form validation that is required
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $this->configuration['extra_classes'] = $form_state->getValue('extra_classes');
  }

}

Then your *.layouts.yml could look like:

advanced_layout_3:
  label: Advanced Layout 3
  category: My Layouts
  class: '\Drupal\my_custom_module\MyLayoutClass'
  template: templates/advanced-layout-3
  library: my_custom_module/advanced-layout-library
  regions:
    main:
      label: Main content

And in your Twig template, you now have access to the {{ settings.extra_classes }} variable. So, your templates/advanced-layout-3.html.twig could look like:

<div class="my-advanced-layout {{ settings.extra_classes }}">
  <div class="main-region">
    {{ content.main }}
  </div>
</div>

Registering a layout as a PHP class with annotations

In all the examples above, we've been using the *.layouts.yml file to register layouts. That is the easiest way to declare a layout if you don't need to give an alternate layout class.

While it's possible to specify an alternate layout 'class' in the *.layouts.yml file, if you have a one-off layout that uses a custom class, it can be easier to register it in PHP with annotations! (Or if your layouts are derivatives, it's the only way, which we'll discuss in more detail in the next section.)

To do this, you put a @Layout annotation above the layout class using the same keys as in the *.layouts.yml. Here's a simple example:

/src/Plugin/Layout/AdvancedLayout4.php

namespace Drupal\my_custom_module\Plugin\Layout;

use Drupal\Core\Layout\LayoutDefault;

/**
 * A very advanced custom layout.
 *
 * @Layout(
 *   id = "advanced_layout_4",
 *   label = @Translation("Advanced Layout 4"),
 *   category = @Translation("My Layouts"),
 *   template = "templates/advanced-layout-4",
 *   library = "my_custom_module/advanced-layout-library",
 *   regions = {
 *     "main" = {
 *       "label" = @Translation("Main content"),
 *     }
 *   }
 * )
 */
class AdvancedLayout4 extends LayoutDefault {
  // Override any methods you'd like to customize here!
}

(Note: declaring layouts this way can ONLY be done from a module. Themes cannot include plugins declared using PHP classes and annotations.)

Registering dynamic layouts using derivatives

So far, we've been registering single layout plugins, one at a time. However, "derivatives" are sets of plugins that are registered dynamically based on some other data.

For example, if you wanted to create a flexible layout builder, you'd give the site administrator some user interface to declare a new layout, and the configuration for each layout would be saved as a config entity. Then you'd register a layout class that used a "deriver" to dynamically register a layout plugin for each of those saved config entities.

So, first you'd register the layout class, for example:

namespace Drupal\my_custom_module\Plugin\Layout;

use Drupal\Core\Layout\LayoutDefault;

/**
 * A layout from our flexible layout builder.
 *
 * @Layout(
 *   id = "flexible_layout",
 *   deriver = "Drupal\my_custom_module\Plugin\Deriver\FlexibleLayoutDeriver"
 * )
 */
class FlexibleLayout extends LayoutDefault {

  /**
   * {@inheritdoc}
   */
  public function build(array $regions) {
    $render_array = parent::build($regions);
    // Since this is a flexible layout builder, you probably need to do
    // something special to render the layout, so we override the ::build()
    // method which is responsible for creating a render array.
    return $render_array;
  }

}

Notice how the annotation is incomplete (i.e., it's missing some of the keys necessary to register a layout) and includes a 'deriver' key. It's the deriver's job to iterate over the data about the layouts and fill in the rest of the information about the layout.

Also, when declaring layouts in this way, unless you can point to a specific 'template' or 'theme', you'll probably need to override the layout's ::build() method to provide custom code for rendering the layout.

Now, here is what an example deriver could look like:

namespace Drupal\my_custom_module\Plugin\Deriver;

use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\my_custom_module\Plugin\Layout\FlexibleLayout;
use Drupal\Core\Layout\LayoutDefinition;

/**
 * Makes a flexible layout for each layout config entity.
 */
class FlexibleLayoutDeriver extends DeriverBase {

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    // Here we need to magically get a list of the config entities.
    // I leave this as an exercise for the reader. :-)
    $config_entities = [];

    // Now we loop over them and declare the derivatives.
    foreach ($config_entities as $entity) {
      // Here we fill in any missing keys on the layout annotation.
      $this->derivatives[$entity->id()] = new LayoutDefinition([
        'class' => FlexibleLayout::class,
        'label' => $entity->label(),
        'category' => $entity->getCategory(),
        'regions' => $entity->getRegions(),
      ]);
    }

    return $this->derivatives;
  }

}

(Note: declaring layouts this way can ONLY be done from a module. Themes cannot include plugins declared using PHP classes and annotations.)

Full annotation reference

Each layout definition must have the following keys:

label
The human-readable name of the layout.
category
The human-readable category to which the layout belongs.
regions
Array of regions in the layout. The keys are the regions' machine names and the values are sub-arrays containing the following elements:
  • label (required): The human-readable name of the region.
theme_hook
If specified, the theme hook which will be used to render this layout. It's expected that the module or theme which registers the layout will also register this theme hook. If you use this key, you cannot use template.
template
If specified, the template to use to render the layout, relative to the given path, without the .html.twig extension. If given, the template will be automagically registered with the theme system. If you use this key, you cannot use theme_hook.

Each layout definition can also have the following optional keys:

default_region
Machine-readable name of the default region.
icon_map
YML structure to describe the icon used in the layout builder interface
description
Optional description of the layout.
path
Path (relative to the module or theme) to resources like icon or template.
library
The asset library to load for this layout. If given, it's expected that the module or theme which registers the layout has also registered the library in its *.libraries.yml file. If you use this key, you cannot use css. Click here for more information about asset libraries in Drupal 8.

Help improve this page

Page status: No known problems

You can: