A “Read More” Teaser is still a common UX pattern; it contains a title, often a summary, sometimes a date or author info, and, of course, a Read More link. The link text itself may differ — “read more”, “learn more”, “go to article”, and so on — but the function is the same: tell the user that what they see is only a preview, and link them to the full content.

Smashing Magazine uses Read More Teasers to display their articles.
While Codrops uses a small “+ more” link
And National Geographic uses an icon and the word “Read”

In 2022, I’d argue that “Read More” links on Teaser components are more of a design feature than a functional requirement; they add a visual call to action to a list of Teasers or Cards, but they aren’t necessary for navigation or clarity.

Although I see fewer sites using this pattern recently, it’s still prevalent enough on the web and in UX designs to justify discussing it.

If you build sites with Drupal, you have a few simple options to implement a Read More Teaser. Both methods have some drawbacks, and both result in adjacent and non-descriptive links.

I’ll explore both options in detail and discuss how to resolve accessibility concerns with a little help from TWIG. (SpoilerTWIG gives us the best markup without any additional modules)

Using Core

Without adding contributed modules or code, Drupal Core gives you a long, rich text field type for storing lengthy content and a summary of that content. It’s a single field that acts like two. In the UI, this field type is called Text (formatted, long, with summary), and it does exactly what the name describes.

Drupal installs two Content Types that use this field type for the Body – Article and Basic Page – but it can be added to any Content Type that needs both a full and a summarized display.

In the Manage Display settings of the Teaser or other summarized view mode, the Summary or Trimmed format is available. The default character limit is 600, but this can be configured higher or lower.

Fig. 1 – The Links field on the Manage Display screen

Finally, in the Manage Display settings, the Links field must be added to render the Read More link.

Fig. 2 – The Links field on the Manage Display screen

That’s all you need to create a Read More Teaser in just a few minutes with Drupal Core, but this method has some shortcomings.

The summary field is easy to miss

Unless you require the summary, it’s often overlooked during content creation. A user has to click the Edit summary link to add a summary, and it’s easy to miss or forget.

Fig. 3 – the easy to miss Edit summary link on content add or edit

You have to choose one or the other format

Notice the word “or” in the Summary or Trimmed format. Out of the gate, you can display what’s in the summary field, in its entirety, or you can display a trimmed version of the Body field. But you can’t display a trimmed version of the summary.

Expect the unexpected when a summary isn’t present

If a summary is not populated, Drupal attempts to trim the Body field to the last full sentence, while staying under the character limit. The problem is the unpredictable nature of content in a rich text field. These may be edge cases, but I’ve seen content editors insert images, headings, dates, and links as the first few elements in the Body field. When the content starts with something other than paragraph text, the Teaser display will, too.

Expect varying length Teasers when a summary is present

Depending on your layout or visual preference, this may not be a concern. But unless you’re judicious about using a standard or similar length for every summary, you’ll have varying length Teasers, which looks awkward when they are displayed side by side.

Fig. 4 – teasers of varying lengths, using Core’s Summary field and Read More link

Using the Smart Trim Module

The contributed Smart Trim module expands on Core’s functionality with additional settings to “smartly” trim the field it’s applied to. The Smart Trim module’s description states:

With smart trim, you have control over:

  1. The trim length
  2. Whether the trim length is measured in characters or words
  3. Appending an optional suffix at the trim point
  4. Displaying an optional “More” link immediately after the trimmed text
  5. Stripping out HTML tags from the field

I won’t cover all of the features of Smart Trim, because it’s well documented already. Smart Trim solves one of the problems noted earlier: it lets you trim the summary. That said, Smart Trim has some of the same drawbacks as Core’s trim feature, along with some limitations of its own.

Sentences aren’t respected

Unlike Core’s trim feature, Smart Trim’s limits don’t look for full sentences. A character or word limitation stops when it hits the defined limit, even if that limit occurs mid-sentence.

Fig. 5 – teasers of varying lengths, using Smart Trim’s word limit at 15 words, and Read More link

Strip HTML might be helpful, or it might not

If your goal is rendering plain text without any HTML (something you might otherwise have to code in a template), Smart Trim’s Strip HTML setting is exactly what you need.

It’s also helpful for dealing with some of those edge cases where content starts with an element other than paragraph text. For example, if the first element in the Body field is an image, followed by paragraph text, the Strip HTML setting works nicely. It strips the image entirely and displays the text that follows in a trimmed format.

But be aware that Strip HTML also strips paragraph tags. If the first element in the Body field is a heading, followed by paragraph text, all tags are stripped, and everything runs together as one large block of text.

“Immediately after” doesn’t mean immediately after

If you want the Read More link to display immediately after the trimmed text, as the module description suggests, you’ll need to assign a class in the Smart Trim settings with a display other than block.

What about adjacent link and accessibility issues?

If you create a Read More Teaser using either the Core or Smart Trim methods described here, there are a couple of problems.

Either method results in markup something like this:

HTML

<article>
  <h2>
    <a href="/article/how-bake-cake">How to Bake a Cake</a>
  </h2>
  <div class="node__content">
    <div class="node__summary">
      <p>Practical tips on cake baking that you can't find anywhere else</p>
    </div>
    <div class="node__links">
      <ul class="links inline">
        <li class="node__readmore">
          <a href="/article/how-bake-cake" title="How to Bake a Cake">Read more</a>
       </li>
      </ul>  
    </div>
  </div>
</article>

The <a> inside the <h2> contains descriptive text, which is necessary according to Google’s Link text recommendations and Mozilla’s Accessibility guidelines for link text.

But the Read More link has text that is not descriptive. Furthermore, it navigates to the same resource as the first <a>, making it an adjacent or redundant link. Without any other intervention, this markup causes repetition for keyboard and assistive technology users.

How do you make it better?

The WCAG has a recommendation for handling adjacent image and text links. You can use this same logic and wrap the entire Teaser content with a single link. But for this, you need TWIG.

Here’s an example of how to structure a TWIG template to create the link-wrapped Teaser.

node–article–teaser.html.twig:

Twig

<article{{ attributes.addClass(classes) }}>
 <div{{ content_attributes.addClass('node__content') }}>
   <a href={{ url }}>
     <div class="wrapper">
       <h2{{ title_attributes.addClass('node__title') }}>
         {{ label }}
       </h2>
       {{ content.body }}
     </div>
   </a>
 </div>
</article>

But while this solves the adjacent link issue, it creates another problem. When you combine this template with the Manage Display settings that insert Read More links, Drupal creates many, many duplicate links.

Fig. 6 – Manage Display settings for Smart Trim’s More link

Instead of wrapping the Teaser content in a single anchor tag, anchors are inserted around every individual element (and sometimes around nothing). The markup is worse than before:

HTML

<div class="node__content">
  <a href="/article/how-bake-cake"> </a>
  <div class="wrapper">
    <a href="/article/how-bake-cake">
      <h2 class="node__title">How to Bake a Cake</h2>
    </a>
    <div class="field field--name--body field--type-text-with-summary">
      <a href="/article/how-bake-cake">  
        <div class="field__label visually-hidden">Body</div>
      </a>
      <div class="field__item">
        <a href="/article/how-bake-cake"></a>
        <div class="trimmed">
          <a href="/article/how-bake-cake">
            <p>Practical tips on cake baking that you can’t find anywhere else</p>
          </a>
          <div class="more-link-x">
            <a href="/article/how-bake-cake"></a>
            <a href="/article/how-bake-cake" class="more-link-x" hreflang="en">More</a>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

But if you remove Smart Trim’s Read more link or Core’s Links field, the Read More text is also gone.

HTML

<div class="node__content">
  <a href="/article/how-bake-cake">
    <div class="wrapper">
      <h2 class="node__title">How to Bake a Cake</h2>
      <div class="field field--name--body field--type-text-with-summary">
        <div class="field__label visually-hidden">Body</div>
        <div class="field__item">
          <p>Practical tips on cake baking that you can’t find anywhere else</p>
        </div>
      </div>
    </div>
  </a>
</div>

Because we’ve already enlisted the help of TWIG to fix the adjacent link issue, it’s easy enough to also use TWIG to re-insert the words “read more.” This creates the appearance of a Read More link and wraps all of the Teaser contents in a single anchor tag.

Twig

<article{{ attributes.addClass(classes) }}>
 <div{{ content_attributes.addClass('node__content') }}>
   <a href={{ url }}>
     <div class="wrapper">
     <h2{{ title_attributes.addClass('node__title') }}>
       {{ label }}
     </h2>
     {{ content.body }}
     <span class="node__readmore">{{ 'Read More'|trans }}</span>
     </div>
   </a>
 </div>
</article>

Resulting HTML:

HTML

<div class="node__content">
  <a href="/article/how-bake-cake">
    <div class="wrapper">
      <h2 class="node__title">How to Bake a Cake</h2>
      <div class="field field--name--body field--type-text-with-summary">
        <div class="field__label visually-hidden">Body</div>
        <div class="field__item">
          <p>Practical tips on cake baking that you can’t find anywhere else</p>
        </div>
      </div>
      <span class="node__readmore">Read more</span>
    </div>
  </a>
</div>

Do it all with TWIG

Of course, you can eliminate Core’s trimming feature, and the Smart Trim module, and do everything you need with TWIG.

Using the same TWIG template, the Body field’s value can be truncated at a particular character length, split into words and appended with “read more” text.

Something like the following should do the trick in most cases.

node–article–teaser.html.twig:

Twig

<article{{ attributes.addClass(classes) }}>
 <div{{ content_attributes.addClass('node__content') }}>
   <a href={{ url }}>
     <div class="wrapper">
       <h2{{ title_attributes.addClass('node__title') }}>
         {{ label }}
       </h2>
       {{ node.body.0.value|striptags|slice(0, 175)|split(' ')|slice(0, -1)|join(' ') ~ '...' }}
       <span class="node__readmore">{{ 'Read More'|trans }}</span>
     </div>
   </a>
  </div>
</article>

Final Thoughts

Today’s internet doesn’t need a prompt on each Teaser or Card to indicate that there is, in fact, more to read. The Read More Teaser feels like a relic from bygone days when, perhaps, it wasn’t obvious that a Teaser was only a summary.

But until we, the creators of websites, collectively decide to abandon the Read More Teaser pattern, developers will have to keep building them. And we should make them as accessible as we can in the process.

Although Drupal provides simple methods to implement this pattern, it’s the templating system that really shines here and solves the accessibility problems.

Are you a developer looking for a new opportunity? Join our team.

Each spring, students at the Rhode Island School of Design (RISD) exhibit their undergraduate and master’s thesis projects at the RISD Museum. Due to Covid-19, they were unable to prepare and stage physical exhibits in the spring of 2020.

Not to be deterred, the school and museum agreed to host the student work online as fully digital exhibits. The Museum previously partnered with Oomph to build out the award-winning “Raid the Icebox” online publication using Drupal and Layout Builder, so it felt familiar and natural to build out similar features for university student projects.

The necessary work involved extending the existing online gallery features to hundreds of additional artists, so we needed to build a system that could scale. Along the way, while we were at it, we were tasked with adding additional features to the platform. Why not have lofty goals?

The Timeline

We kicked off the first stage of the project on April 20, 2020, aiming for a two-staged release. Most of the new code would need to be deployed by the last week of May, with the additional features released two weeks later. The basic infrastructure would have to be established along with the custom permissions for artists, editors, and museum administrators. A second stage would add refinement to font selection and color palette.

What the Artists Needed

Department index of the final site, showing each department and each artist/designer featured on the sites

What the Staff Needed

Some new features added include bulk user import and node clone — Notice the “User Import” option in this screenshot of the Configuration menu from the Drupal admin interface

A Deeper Dive

Overall Approach

We leveraged Drupal to build out our new features “the Drupal way.” The Node Clone and Bulk User Import modules could be installed and enabled with our Composer workflow and used right out of the box to offer additional powerful functionality. Now a user with the Editor role could craft a meticulously designed template and then clone it for other school departments. A user with the Web Administrator role would not have to add users one-by-one through the user interface but could import large numbers of new users — while specifying the user role — with CSV files.

We added the new custom fields, content types, user roles, and text formats manually through Drupal’s UI. We could later use preprocess functions in the theme and Twig templates to render content as needed.

There were a lot of fields needed, covering different aspects of the typography. Here are a few:RISD

Managing fields for a custom content type — screenshot of the Manage Fields Drupal admin interface

Since it was a Drupal 8 project, we made extensive use of config sync to export and import config files. The front-end and back-end developers could work independently until it was time to merge branches for testing. Then we were able to seamlessly push changes to higher environments as part of our deploy process.

Note: As a rule, we recommend setting config to read-only, especially on projects that have many web admin users.

Custom Webfont Example

With those new fields in place, a user sees text input fields on the node edit view of each publication to enter in custom font URLs or names.

Custom fields to manage typography on a publication — screenshot of the Drupal admin interface showing fields for Primary webfont URL, Webfont family name, and webfont type (sans, serif, or mono)

In terms of rendering to the page when someone is viewing the node, this requires both a preprocess hook in the [custom_theme].theme file and changes to the Twig template.

Note: Please be aware that allowing hundreds of users to input free text is not an ideal situation, and that security measures should be taken when processing free text.

Here is what the preprocess hook looks like for the mytheme.theme file:

use Drupal\node\Entity\Node;
use Drupal\taxonomy\TermStorage;

/**
 * Implements hook_preprocess_HOOK().
 */
function mytheme_preprocess_html(array &$variables) {
  $routeMatch = Drupal::routeMatch();
  $node = $routeMatch->getParameter('node');

  if ($node instanceof Node && $node->getType() === 'publication’) {

    if (isset($node->field_primary_webfont_url) && !$node->field_primary_webfont_url->isEmpty()) {
      $variables['primary_webfont_url'] = $node->field_primary_webfont_url->value;
      $variables['primary_webfont_family'] = $node->field_primary_webfont_family->value;
      $variables['primary_webfont_type'] = $node->field_primary_webfont_type->value;
    }

PHP

Then in the Twig template, which is at this path: myproject/docroot/themes/custom/mytheme/templates/layout/html.html.twig

<!DOCTYPE html>
<html{{ html_attributes }}>
  <head>
    <title>{{ head_title }}</title>

    {% if primary_webfont_url|length %}
      <link rel="stylesheet prefetch" media="screen" href="{{ primary_webfont_url }}">
      <style type="text/css">
        :root {
          --ff__serif: '{{ primary_webfont_family }}', {{ primary_webfont_type }};
        }
      </style>
    {% endif %}

    {% if secondary_webfont_url|length %}
      <link rel="stylesheet prefetch" media="screen" href="{{ secondary_webfont_url }}">
      <style type="text/css">
        :root {
          --ff__sans: '{{ secondary_webfont_family }}', {{ secondary_webfont_type }};
        }
      </style>
    {% endif %}

    {% if background_color_override|length and foreground_color_override|length %}
      <style type="text/css">
        :root {
          --c__primary--bg: {{ background_color_override }};
          --c__primary--fg: {{ foreground_color_override }};
        }
      </style>
    {% endif %}

  </head>
  <body{{ attributes }}>
    {{ page_top }}
    {{ page }}
    {{ page_bottom }}
  </body>
</html>

HTML

Finally, here is what someone viewing a page would see:

Grad show artist Lilla E. Szekely sets custom system fonts and background/foreground colors Color Palette Selection example

Most of the creative work for each piece of content happened behind the scenes in Layout Builder. Each block or section could be configured individually, which gave the artists a lot of ability to customize their online territories to the fullest extent possible.

In addition to being able to choose a foreground or background color on the node level, an artist or editor can choose to change the color of just one block in Layout Builder simply by clicking on the “Style Settings” link.

Screen capture of the contextual floating drop down menu for “Style Settings” in the Drupal admin

Another inline-editing window will pop up with additional options. In the “Add a style” dropdown menu, the artist or editor can select “Component Background Color,” click “Add Styles,” and choose from one of the colors in the palette to be applied to the block.

The Drupal admin interface showing a select list for background color on any component, Section or Block

Along with the preprocessing described in the previous section, we extended Layout Builder’s features with a custom module to alter layouts. The plugin class lives at: docroot/modules/custom/my_module/Plugin/Layout/LayoutBase.php

<?php

namespace Drupal\my_module\Plugin\Layout;

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

/**
 * Provides a layout base for custom layouts.
 */
abstract class LayoutBase extends LayoutDefault implements PluginFormInterface {

  public const NO_BACKGROUND_COLOR = 0;

  public function build(array $regions): array {
    $build = parent::build($regions);
    $backgroundColor = $this->configuration['background_color'];
    if ($backgroundColor) {
      $build['#attributes']['class'][] = 'rpp__bg-color--' . $backgroundColor;
    }
    return $build;
  }

  public function defaultConfiguration(): array {
    return [
      'background_color' => NO_BACKGROUND_COLOR,
      'id' => NULL,
      'background_color_override' => NULL,
    ];
  }

  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
    $form['background'] = [
      '#type' => 'details',
      '#title' => $this->t('Background'),
      '#open' => TRUE,
      '#weight' => 20,
    ];
    $form['background']['background_color'] = [
      '#type' => 'radios',
      '#default_value' => $this->configuration['background_color'],
    ];
    $form['background']['overrides'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Overrides'),
    ];
    $form['background']['overrides']['background_color_override'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Background Color'),
      '#default_value' => $this->configuration['background_color_override'],
      '#attributes' => [
        'placeholder' => '#000000',
      ],
    ];
    return $form;
  }

  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();
    $this->configuration['background_color'] = $values['background']['background_color'];
    $this->configuration['id'] = $values['extra']['attributes']['id'];
    $this->configuration['background_color_override'] = $values['background']['overrides']['background_color_override'];
  }

}

PHP

The Background Color form gets inserted into the Layout Builder form, and the user’s color selections get submitted and saved to configuration in the submitConfigurationForm() method.

The custom layout needs to be registered, so it should be added in a file called: my_module.layouts.yml and looks like:

layout_base:
  label: 'New Layout'
  category: 'Custom Layouts'
  template: templates/layout-base
  default_region: main
  regions:
    main:
      label: Main content

PHP

Now this custom layout with color overrides and whatever else you want to add will be available for users with the appropriate permissions to edit content in Layout Builder.

Conclusion

Jeremy Radtke, Assistant Director of Digital Initiatives at the RISD Museum, said in a recent presentation to the Museum Publishing Digital Interest Group that RISD sees the museum as a site of creative collaboration. In terms of the end-of-year digital showcase, this is demonstrated in the emphasis on student artists having a high degree of creative control over their projects. They were able to radically alter the existing layout templates offered to them, changing fonts, colors, and other elements of the theme. They were able to configure blocks to add static images, animated gifs, and other media files such as short films to stretch the limits of the digital space.

There were a total of 700 undergraduates and grads featured in the online exhibit, which featured 16 departments. The art school is attached to the RISD Museum, and Radtke said the museum’s style is very much along the lines of an art school, in that it employs critique — asking questions and solving problems is strongly emphasized. This project was about content delivery, but also how to generate content. Oomph was proud to be part of that collective journey of exploration and experimentation.


Related Resources


This post will assume you have already completed the base setup of enabling Layout Builder and added the ability to manage layouts to one of your content types. If you are not to this point check out Drupal.orgs documentation on layout builder or this article by Tyler Fahey which goes over setup and some popular contrib module enhancements.

As we mentioned in part 1 of this series, you should expect a little DIY with Layout Builder. So far the best way we have found to theme Layout Builder is by creating a custom module to provide our own custom layouts and settings. By defining custom layouts in a custom module we get the ability to control each layout’s markup as well as the ability to add/remove classes based on the settings we define.

Writing the custom layout module

Setup the module

Start by creating your custom module and providing the required .info.yml file.

demo_layout.info.yml:

name: Demo Layout
description: Custom layout builder functionality for our theme.
type: module
core: 8.x
package: Demo

dependencies:
  - drupal:layout_builder

YAML

Remove default core layouts

Layout Builder comes with some standard layouts by default. There’s nothing wrong with these, but generally for our clients, we want them only using our layouts. This hook removes those core layouts, leaving only the layouts that we will later define:

demo_layout.module

/**
 * Implements hook_plugin_filter_TYPE__CONSUMER_alter().
 */
function demo_layout_plugin_filter_layout__layout_builder_alter(array &$definitions): void {
  // Remove all non-demo layouts from Layout Builder.
  foreach ($definitions as $id => $definition) {
    if (!preg_match('/^demo_layout__/', $id)) {
      unset($definitions[$id]);
    }
  }
}

PHP

Register custom layouts and their regions

The next step is to register the custom layouts and their respective regions. This process is well documented in the following drupal.org documentation: https://www.drupal.org/docs/8/api/layout-api/how-to-register-layouts

For this particular demo module we are going to define a one column and a two column layout. These columns will be able to be sized later with the settings we provide.

demo_layout.layouts.yml

demo_layout__one_column:
  label: 'One Column'
  path: layouts/one-column
  template: layout--one-column
  class: Drupal\demo_layout\Plugin\Layout\OneColumnLayout
  category: 'Columns: 1'
  default_region: first
  icon_map:
    - [first]
  regions:
    first:
      label: First

demo_layout__two_column:
  label: 'Two Column'
  path: layouts/two-column
  template: layout--two-column
  class: Drupal\demo_layout\Plugin\Layout\TwoColumnLayout
  category: 'Columns: 2'
  default_region: first
  icon_map:
    - [first, second]
  regions:
    first:
      label: First
    second:
      label: Second

YAML

Pay close attention to the path, template, and class declarations. This determines where the twig templates and their respective layout class get placed.

Creating the base layout class

Now that we have registered our layouts, it’s time to write a base class that all of the custom layouts will inherit from. For this demo we will be providing the following settings:

However, there is a lot of PHP to make this happen. Thankfully for the most part it follows a general pattern. To make it easier to digest, we will break down each section for the Column Width setting only and then provide the entire module at the end which has all of the settings.

src/Plugin/Layout/LayoutBase.php

<?php
  declare(strict_types = 1);

  namespace Drupal\demo_layout\Plugin\Layout;

  use Drupal\demo_layout\DemoLayout;
  use Drupal\Core\Form\FormStateInterface;
  use Drupal\Core\Layout\LayoutDefault;

  /**
   * Provides a layout base for custom layouts.
   */
  abstract class LayoutBase extends LayoutDefault {

  }

PHP

Above is the layout class declaration. There isn’t a whole lot to cover here other than to mention use Drupal\demo_layout\DemoLayout;. This class isn’t necessary but it does provide a nice one-stop place to set all of your constant values. An example is shown below:

src/DemoLayout.php

<?php

declare(strict_types = 1);

namespace Drupal\demo_layout;

/**
 * Provides constants for the Demo Layout module.
 */
final class DemoLayout {

  public const ROW_WIDTH_100 = '100';

  public const ROW_WIDTH_75 = '75';

  public const ROW_WIDTH_50 = '50';

  public const ROW_WIDTH_25 = '25';

  public const ROW_WIDTH_25_75 = '25-75';

  public const ROW_WIDTH_50_50 = '50-50';

  public const ROW_WIDTH_75_25 = '75-25';

}

PHP

The bulk of the base class logic is setting up a custom settings form using the Form API. This form will allow us to formulate a string of classes that get placed on the section or to modify the markup depending on the form values. We are not going to dive into a whole lot of detail as all of this is general Form API work that is well documented in other resources.

Setup the form:

/**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {

    $columnWidths = $this->getColumnWidths();

    if (!empty($columnWidths)) {
      $form['layout'] = [
        '#type' => 'details',
        '#title' => $this->t('Layout'),
        '#open' => TRUE,
        '#weight' => 30,
      ];

      $form['layout']['column_width'] = [
        '#type' => 'radios',
        '#title' => $this->t('Column Width'),
        '#options' => $columnWidths,
        '#default_value' => $this->configuration['column_width'],
        '#required' => TRUE,
      ];
    }

    $form['#attached']['library'][] = 'demo_layout/layout_builder';

    return $form;
  }

 /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
  }

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

 /**
   * Get the column widths.
   *
   * @return array
   *   The column widths.
   */
  abstract protected function getColumnWidths(): array;

PHP

Finally, we add the build function and pass the column width class:

/**
   * {@inheritdoc}
   */
  public function build(array $regions): array {
    $build = parent::build($regions);

    $columnWidth = $this->configuration['column_width'];
    if ($columnWidth) {
      $build['#attributes']['class'][] = 'demo-layout__row-width--' . $columnWidth;
    }

    return $build;
  }

PHP

Write the column classes

Now that the base class is written, we can write column-specific classes that extend it. These classes are very minimal since most of the logic is contained in the base class. All that is necessary is to provide the width options for each individual class.

src/Plugin/Layout/OneColumnLayout.php

<?php

declare(strict_types = 1);

namespace Drupal\demo_layout\Plugin\Layout;

use Drupal\demo_layout\DemoLayout;

/**
 * Provides a plugin class for one column layouts.
 */
final class OneColumnLayout extends LayoutBase {

  /**
   * {@inheritdoc}
   */
  protected function getColumnWidths(): array {
    return [
      DemoLayout::ROW_WIDTH_25 => $this->t('25%'),
      DemoLayout::ROW_WIDTH_50 => $this->t('50%'),
      DemoLayout::ROW_WIDTH_75 => $this->t('75%'),
      DemoLayout::ROW_WIDTH_100 => $this->t('100%'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  protected function getDefaultColumnWidth(): string {
    return DemoLayout::ROW_WIDTH_100;
  }
}

PHP

src/Plugin/Layout/TwoColumnLayout.php

<?php

declare(strict_types = 1);

namespace Drupal\demo_layout\Plugin\Layout;

use Drupal\demo_layout\DemoLayout;

/**
 * Provides a plugin class for two column layouts.
 */
final class TwoColumnLayout extends LayoutBase {

  /**
   * {@inheritdoc}
   */
  protected function getColumnWidths(): array {
    return [
      DemoLayout::ROW_WIDTH_25_75 => $this->t('25% / 75%'),
      DemoLayout::ROW_WIDTH_50_50 => $this->t('50% / 50%'),
      DemoLayout::ROW_WIDTH_75_25 => $this->t('75% / 25%'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  protected function getDefaultColumnWidth(): string {
    return DemoLayout::ROW_WIDTH_50_50;
  }
}

PHP

We can now check out the admin interface and see our custom form in action.

One column options:

Two column options:

Add twig templates

The last step is to provide the twig templates that were declared earlier in the demo_layout.layouts.yml file. The variables to be aware of are:

src/layouts/one-column/layout–one-column.html.twig

{#
/**
 * @file
 * Default theme implementation to display a one-column layout.
 *
 * Available variables:
 * - content: The content for this layout.
 * - attributes: HTML attributes for the layout <div>.
 * - settings: The custom form settings for the layout.
 *
 * @ingroup themeable
 */
#}

{%
  set row_classes = [
    'row',
    'demo-layout__row',
    'demo-layout__row--one-column'
  ]
%}

{% if content %}
  <div{{ attributes.addClass( row_classes|join(' ') ) }}>
    <div {{ region_attributes.first.addClass('column', 'column--first') }}>
      {{ content.first }}
    </div>
  </div>
{% endif %}

Twig

src/layouts/two-column/layout–two-column.html.twig

{#
/**
 * @file
 * Default theme implementation to display a two-column layout.
 *
 * Available variables:
 * - content: The content for this layout.
 * - attributes: HTML attributes for the layout <div>.
 * - settings: The custom form settings for the layout.
 *
 * @ingroup themeable
 */
#}

{# Get the column widths #}
{% set column_widths = settings.column_width|split('-') %}

{%
  set row_classes = [
    'row',
    'demo-layout__row',
    'demo-layout__row--two-column'
  ]
%}

{% if content %}
  <div{{ attributes.addClass( row_classes|join(' ') ) }}>

    {% if content.first %}
      <div {{ region_attributes.first.addClass('column', 'column--' ~ column_widths.0, 'column--first') }}>
        {{ content.first }}
      </div>
    {% endif %}

    {% if content.second %}
      <div {{ region_attributes.second.addClass('column', 'column--' ~ column_widths.1, 'column--second') }}>
        {{ content.second }}
      </div>
    {% endif %}

    </div>
  </div>
{% endif %}

Twig

Notice settings.column_width was passed with a string: 75-25. We need to split it and place each value on our column which results in the following output.

<div class="demo-layout__row-width--75-25 row demo-layout__row demo-layout__row--two-column ">
  <div class="column column--75 column--first"></div>
  <div class="column column--25 column--second"></div>
</div>

HTML

Since these are custom classes, and we haven’t written any CSS, these columns do not have any styling. Depending on your preference, you can implement your own custom column styles or wire up a grid framework such as Bootstrap in order to get the columns to properly size themselves.

Wrapping it up

You should be at a point where you have an idea of how to create custom settings in order to theme layout builder sections. You can take this method and extend it however you need to for your particular project. There’s no definitive best way to do anything in the world of web development, and Layout Builder is no exception to that rule. It’s a great addition to Drupal’s core functionality, but for larger sites, it likely won’t be and shouldn’t be the only way you handle layout. Much like Drupal itself though, as more and more people use it, Layout Builder will only become stronger, more robust, more fully-featured, and better documented. If it doesn’t seem like a good fit for you right now, it may become a better fit as it grows. If it does seem like a good fit, be ready to get your hands dirty!

The full demo layouts module with all of the custom settings is available here: https://github.com/oomphinc/layout-builder-demo/tree/master/moduleexamples/demolayout

With the release of Drupal 8.7 in May of 2019 came the rollout of the much-anticipated Layout Builder core module. According to Drupal.org, Layout Builder allows content editors and site builders to easily and quickly create visual layouts for displaying content by providing the ability to drag and drop site-wide blocks and content fields into regions within a given layout. Drupalists were excited about it, and so were we.

For a long time, we developed and came to heavily rely on our own extension of the Paragraphs module to give content managers the power to build and modify flexible layouts. When we heard that there would now be an equivalent option built right into core, we thought, “could this be the end of Paragraphs?” Well, the only way to find out is to dig in and start using it in some real-world scenarios.

Layout Builder is still new enough that many how-to guides only focus on installing it and enabling it on a content type or two and overviews of options that are available right out of the box. That’s probably fine if your use-case is something akin to Umami, Drupal.org’s example recipe site. But if you want to use it in a significant way on a larger site, it probably won’t be long before you want to customize it to fit your situation. Once you get to that point, documentation becomes scant. If you’ve already got some experience rolling your own extension of a module or at least writing preprocesses, you’re more likely to get better mileage out of your experience with Layout Builder.

First, let’s take a look at some of the pros and cons of using Layout Builder. If there are any deal-breakers for you, it’s better to identify them sooner than later.

Layout Builder Pros:

1. All core code

Yes, the fact that Layout Builder is a core initiative that will continue to get attention and updates is fantastic no matter how stable similar module initiatives might be. As it’s core, you get great integration for language translation.

2. Block-based, but supports fields as well

Blocks are a familiar core Drupal content paradigm. They can be used as one-off content containers or as repeatable containers for content that appears in multiple places but should be edited from a single location. Fields can also be placed as content into a layout, which makes building custom templates that can continue to leverage fields very flexible.

3. Better WYSIWYG authoring experience

End-users will be much happier with the (not quite) WYSIWYG editing experience. While it is not directly one-to-one, it is much better than what we have seen with Paragraphs, which is a very “Drupal” admin experience. In many ways, previously, Preview was needed to know what kind of design your content was creating.

4. Supports complex design systems with many visual options

Clients can get quite a bit of design control and can see the effects of their decisions very quickly. It makes building special landing pages very powerful.

5. Plays nice with Clone module

While custom pages described in Pro #4 are possible, they are time-consuming to create. The Clone module is a great way to make copies of complex layouts to modify instead of starting from scratch each time.

6. “Locked” Layouts are the default experience

While complex custom pages are possible, they are not the default. Layout Builder might have been intended to replace custom template development, because by default when it is applied to a content type, the option to override the template on a node-by-node basis is not turned on. A site builder needs to decide to turn this feature on. When you do, proceed with caution.

Layout Builder Cons

1. Lack of Documentation

Since LB is so relatively new, there is not an abundance of documentation in the wild. People are using it, but it is sort of still the Wild Wild West. There are no established best practices on how to use it yet. Use it with Paragraphs? Maybe. Use it for the entire page, including header and footer? You can. Nest it in menus? We’ve done it. Should we have done it? Time will tell.

2. More time is required to do it well

Because of Con #1, it’s going to take more time. More time to configure options, more time to break designs down into repeatable components, and more time to test all the variations and options that design systems present.

3. Admin interface can conflict with front-end styles

While Pro #3 is a great reason to use LB, it should be known that some extra time will be needed to style the admin mode of your content. There is some bleeding together of admin and non-admin front-end styles that could cause your theme build to take longer.

An example: We created a site where Layout Builder custom options could control the animation of blocks. Great for the front-end, but very annoying for the backend author when blocks would animate while they were trying to edit.

4. Admin editing experience still in its infancy

Again, while Pro #3 is significant, the current admin editing experience is not the best. We know it is being worked on, and there are modules that help, but it is something that could tip the scales depending on the project and the admin audience.

5. Doesn’t play nice with other template methods

Which is to say that you can’t easily have a page that is partially LB and partially a templated View or something else. You can create a View that can be placed into a Block that is placed via Layout Builder, but you can’t demarcate LB to one section of a page and have a View or the body field in the other.

6. Content blocks do not export with configuration

As blocks go, the configuration of a block is exportable, but the content isn’t. Same with the blocks that Layout Builder uses, which can make keeping staging/production environments in sync frustrating. Just like with Paragraphs or custom blocks, you’ll have to find a reliable way of dealing with this.

7. Overriding a default layout has consequences

We have seen this ourselves first-hand. The design team and client want a content type to be completely flexible with Layout Builder, so the ability for an author to override the default template is turned on. That means the node is now decoupled from the default template. Any changes to the default will not propagate to those nodes that have been decoupled and modified. For some projects, it’s not a big deal, but for others, it might become a nightmare.

8. The possibility of multiple design options has a dark side

Too many options can be a bad thing. It can be more complex for authors than necessary. It can add a lot more time to theming and testing the theme when options create exponential possibilities. And it can be very hard to maintain.


With great power comes great responsibility. Layout Builder certainly gives the Drupal community great power. Are we ready for the caveats that come with that?

Ready to tackle customizing Layout Builder? Watch for Part Two, where we’ll dive into defining our own layouts and more.

Over the past week Kathy Beck and I have had the pleasure of touring a talk that we have prepared around Drupal 8’s Layout Builder. We aren’t the only ones talking about it, of course, but it is a set of tools in Drupal core that have lately found new interest in the community. More and more developers are discovering and using the tool, which makes it an exciting bit of technology to talk about.

We recently had great success with Layout Builder on a new project. What was a really nice was that our design system paradigm from previous projects was easily portable into this new Layout Builder tool. So our UX thinking was solid, and this was a solid tool that could continue to support that way of working.

Moving into Layout Builder also gave us some additional advantages:

What Template Control in Layout Builder looks like

For most projects, the key advantage to Layout Builder is that it puts the creation and “design” of a content type’s main template in the admin experience. Drupal already puts many controls in the Admin experience, allowing site builders to create content types, configure the fields that they use, and even configure some of the ways in which that data will be displayed to users. With that, it makes sense that Layout Builder provides way in which site builders can create visual templates.

This reduces the need for front-end templates in Twig. Again, since a site builder is the one to configure a new content type directly in the admin, they can now also create that default template in the admin as well. Just like theming in Twig, though, if a site builder makes a change to the main template, any piece of content created with that template will also update. Its a powerful way to edit and control templates per content type.

What’s really cool is that we as the site builders can decide which content type template’s an author has access to override the layout of. The scenario is this: An article content type is locked down, and the author can only access the fields to update title, image, and body content. But a “Marketing page” content type has that restriction removed, so an author has access to “Layout”, and therefore they can make as many changes to that page as they want. They can add new content components, they can delete others, change color, column design, and anything else that we create to modify designs.

Watch the Videos

With that explanation, our talks go into more detail about how this all works and what problems we wanted to try to solve. The first video that we have ready to view was geared towards a design audience. Another one to come along soon was geared towards a more technical, Drupal-knowledgable audience. Pick the one that is right for you!

Oh, and as a “cool to know”, the presentation deck itself was built in Layout Builder!

Presentation in front of a Developer Audience for DrupalCamp Atlanta:

Presentation in front of a Design Audience for DesignWeek RI:

Oomph has been using Paragraphs to deliver “flexible content” areas and content layouts for our clients. With the release of Drupal 8.7 and the addition of Layout Builder, Oomph has begun to incorporate the new Layout Builder functionality into our websites. Learn the advantages of Layout Builder over Paragraphs and how you can successfully implement Layout Builder on your next project.

Some topics covered are:

Watch Senior Drupal Architect John Picozzi and <Senior UX Engineer Kathy Beck from their Design4Drupal 2019 talk on all things Layout Builder.

Watch the Design4Drupal talk on Drupal Layout Builder

Traditional pages should be a thing of the past. The modern web experience is composed of small pieces of content arranged into a cohesive story. For an author, a single content area is fine for a blog post—a single story that expounds upon one thought. But for a modern website with many landing pages—places where the user needs to decide what they want to do next—a single content column will no longer suffice. We need modular content and a better way to manage its creation.

At An Event Apart this April, Ethan Marcotte was talking about the future of design and User Experience in a multi-device world. Specifically, he said that we should:


[…] design networks of content that are composed of patterns. These patterns, which are small responsive patterns themselves, can be stitched together to create composite experiences like pages.


— From Luke W.’s notes

In other words, the idea of “pages” needs to be blown apart into smaller patterns. Break free from the single content area and create compelling experiences for a modern audience!

We at Oomph agree, and the roadmap that we drew last summer while designing and developing the new BCBS.com (launched in November, 2016) was already blowing the old idea of pages apart with a modular content system.

How did we get here?

Why have we been bound by a single content area? This all goes back to the prevalence of blogs and content management systems (CMSs), which, for ease of use, created just one main content area. For an author of a blog post, one content area is enough as they are writing a single story. But for business sites that need to market a product or service, it is usually not enough.

For Blue Cross and Blue Shield Association, when we were discovering ways in which we could redesign their site, we recognized that they needed to break out from the constraints of the single content area provided by CMSs. The new BCBS.com was going to be built on Drupal 8, so we needed to think about solutions within that ecosystem.

Why we Chose the Drupal Paragraphs Module

Modern CMSs have started to handle multiple chunks of content, and within the Drupal community there are a few ways to address it. The downfall—in our minds—with a few of these solutions is that they put all of the power into the hands of the authors. If you have a team of savvy people that understand interface design, responsive design, and are not afraid to get their hands dirty with some HTML, that can work. In our experience, though, it is too much power and authors are more liable to break something—either break the rendering of the page or break too far outside the brand guidelines. These solutions are difficult to use and typically get abandoned by the author.

Instead of unlimited options, what we as the design and development team wanted was the ability to define our own set of options that we could then expose to the authors. We’d build each set of options into a small responsive pattern, which allows the author to concentrate on their content and how they want to tell a story.

The Paragraphs Module can do exactly that. It adds a new field type that works like Entity References, and with it, we can define all sorts of Paragraph Types and configurable options to give to authors. For the end viewer, the experience has visual variety, but it never veers off brand or looks broken. For the author, they have many options but do not need to think about what happens to their content for mobile or tablet. Those decisions have been baked into each Paragraph Type.

What we Built for BCBSA

We based our modular design patterns on a simple “card” consisting of an optional icon, an optional background (could be color, image, or gradient), and a WYSIWYG content field. These elements had many, many options associated with them:

From here, we defined 10 different Paragraph “Rows” of cards and other types of content. Each of these bundles have responsive design built in — they are small responsive patterns that can be stitched together to create pages:

With these options and rows together, the BCBSA team can create fantastic long-form content pages as well as complex landing pages that can still use Drupal’s Body Content area, Featured Images, and even Blocks and Views.

Oomph & BCBSA ❤ Paragraphs

With Drupal and Paragraphs, both the Oomph and BCBSA teams are very excited about the extensive possibilities of a curated system of modular components, options, and rows. Authors have a large but controlled set of options which makes every page feel unique but on brand, and the viewers get a fully responsive experience without any additional work.

We’ve become believers in this kind of system and have already started adapting it to other clients who need to solve a similar problem. We could design a custom system of Paragraphs and options for you, too.


Resources: