When designing a Drupal based site, there is a killer tool with which most of us are familiar, Paragraphs. Paragraphs allows us to create a component-based architecture, whereby authors can add components to a page in a structured way. How can we use the flexibility of Paragraphs to create a component-driven author experience that empowers the author while ensuring a consistent design language?
Stuff Used In This Post:
- Drupal core ( required )
- Paragraphs ( required )
- Foundation ( optional, but really helpful )
- Field Group ( optional, but a good idea )
- Fences ( optional, but a good idea )
- Markup ( optional, but really encouraged )
A bit about Paragraphs
If you are not familiar with Paragraphs, I encourage you to take a look at the project page. The short version is, Paragraphs allows you to define a "bundle" of fields that an author can optionally include in a piece of content. By defining and configuring different bundles, a site builder can give authors multiple options for the types of content available in a piece of content.
By default, Drupal gives site builders the ability to add fields to a content type. Let's say you have a Basic Page content type, out-of-the-box, a Title field and Body field are included. You can also add many different field types, such as an image field, telephone number field, select lists, taxonomy fields, etc. Drupal core ships with many types of fields, and even more are available as contributed modules.
After you add fields to a content type, you can set the order in which those form fields appear to the author, and, independently, set the order in which the field values are displayed to readers. Unfortunately, that doesn't give authors much control over the composition of a page. The architect must anticipate every field the author might need, and once the display is configured, there is little flexibility left for the author.
The new Layout Builder module ( hopefully soon to be included in Drupal core ) gives a bit more control as to how fields are laid out, but there are many things that Layout Builder can not do, such as configure padding, colors, or other attributes that an author may want to configure. Additionally, all of the fields must still be added at the content type level.
Let's say we know that an author will want to be able to add some images and text to a Basic Page. We would need an image field and a text field. But then, what if the author wants the page to have an image at the top, above the text, and another below the text? We would then need to add two image fields, and a single text field. As the author's requirements increase, so does the complexity of the Basic Page edit screen. The problem is, we can not possibly anticipate every type of layout / content that an author might want.
Paragraphs can enhance the author experience by offering optional fields that can be added to a piece of content "on demand". Paragraphs work by creating "bundles" of fields that are then added to the content type. Then the author can select the "bundle" with a single button and new fields are added to the content type while editing a page. This means that a content type is no longer limited by the site builder's initial understanding of every field that a piece of content will need, nor is the display of those fields predetermined.
How Paragraphs enable a Component Driven Site
Using Paragraphs, we can create two bundles, Text and Image. Then, on our Basic Page content type, we can add a single field, Content ( using the required Entity Reference Revisions field type ) that reference our two bundles. By setting the limit to "Unlimited" in the field storage configuration, the author can now add as many Text field and Image fields as necessary to their new page. On the edit form, they are presented a button ( several different widgets are available ) to add which ever field they want, and as many as they like. They can also reorder the fields as they see fit. This opens up many possibilities.
This ability to add different fields to a piece of content on demand gives the site builder the opportunity (responsibility?) to componentize the site. It no longer makes sense to style a site based on the content type or individual page, because we no longer have control over exactly what content will be on the page. Rather, we need to style the components that may be present on the page. This way, regardless of how many components, or in what order, the components are added, the site design remains cohesive.
Giving Authors even more control
So far, we have given authors the ability to add as many images and blocks of text as they desire, and select the order in which they appear. While that is a great first step, we want to give them even more control, and more field options. Maybe under some blocks of text the author wants to add a link, and have it styled as a button. We can add another Paragraph bundle that includes a Link field. Since every Paragraph bundle has a name, let's call this "Button". Now the author can add an image, text, and a link to the page. But the link is just some underlined text, how do we ensure all of the link fields appear as a button, without requiring authors to add CSS classes to the link?
Well, I tend to do this with a combination of Preprocess functions and CSS.
This goes in the THEME.theme file. THEME would be the machine name of your theme. Keep in mind, you will want to use a custom theme ( or child theme ).
// file: THEME.theme
/**
* Implements hook_preprocess_paragraph__type().
*/
function THEME_preprocess_paragraph__button(array &$variables) {
// get the paragraph from the $variables array
$paragraph = $variables['paragraph'];
// add a class to the Paragraph container so we can target with CSS
$variables['attributes']['class'][] = 'button--configuration';
}
Next, in the CSS, you could add something like:
// file: style.css
.button--configuration a {
// button styles
}
There is a reason that I put the class on the container rather than the a element directly which I will get to, but you may choose to do this differently.
Also, some of you may be saying, "Hey, we can do that with Twig rather than PHP!" And you would be correct. The reason I am not doing this is so that I don't need to create dozens of Twig files. By adding the classes through preprocess functions, I can reuse the same Twig files and keep my styles better scoped. There is also a case for using a combination of preprocess and Twig, which is how I usually do this.
NOTE: Going forward, I am going to use Sass instead of pure CSS. For one thing, it helps componentize my styles in individual partials. For another, I am going to be using Foundation for quickly building out components. This should work for Bootstrap or any other front-end framework you choose ( even your own custom framework ). There is a Drupal theme, Zurb Foundation, but it's currently a bit behind on the Foundation version. I tend to use Foundation directly in a custom theme, but the concepts here mostly apply. This is not a tutorial on Sass, so if you are not familiar, it's definitely worth looking into.
So, to change the button to a component to Sass using Foundation, I would create a partial _paragraphs--button.scss in a directory like scss/components/.
// file: _paragraphs--button.scss
.button--configuration {
a {
@include button; // this is the Foundation mixin for creating a button.
}
}
Now whenever the author adds a link using our button field, it will get the button styles.
In Foundation, and probably most frameworks, buttons have a default style, usually a standard background color, text color, and some other settings. Let's say we want the author to be able to set the background color by themselves. Foundation has some default classes for setting button backgrounds ( like primary, secondary, etc ). Again, we don't want to burden authors with implementation details, like class names. Also, primary and secondary don't really mean anything to an author, they just want to select a color. And we want to limit those color options to those that work with our theme.
Typically, when I design a site, one of the first things I do is establish a color palette. The palette will have the customer's brand colors and a few variations of those colors. I would include the colors as a Sass map:
// file: _vars.scss
$color-palette: (
blue: #0000ff,
red: #ff0000,
green: #00ff00
);
Now I an reuse those colors across my site to ensure consistency.
So, how do I give those options to my authors? I would add a field to my Button paragraph bundle call "Button Color" ( machine name: field_button_color ). I typically use the field type List Text. For the allowed values, I enter something like this:
blue|Blue
red|Red
green|Green
This will create a select list for the author with the allowed colors ( It can also be radio buttons / checkboxes ). In the field settings, I make it required, and set a default value. For usability, I generally move these "Configuration" fields above the "Content" field in Manage Form Display, and move to the Hidden area in Manage Display on my Paragraph bundle. This way, the field is available to my author, but never displayed to visitors.
There is also a way to do this with a color picker field, but it adds some complexity. Maybe one day I will write a post about it.
Next, I add my field to my preprocess function:
<?php
// file: THEME.theme
/**
* Implements hook_preprocess_paragraph__type().
*/
function THEME_preprocess_paragraph__button(array &$variables) {
// get the paragraph from the $variables array
$paragraph = $variables['paragraph'];
// add a class to the Paragraph container so we can target with CSS
$variables['attributes']['class'][] = 'button--configuration';
// check for the field_button_color and set a class.
if ($paragraph->hasField('field_button_color') && !$paragraph->field_button_color->isEmpty()) {
$variables['attributes']['class'][] = 'button-color--' . $paragraph->field_button_color->value;
}
}
Here, what I did was check to see if that field is added to my paragraph bundle, and has a value set. This is just a good practice, because if you have added the field after some content was created, it will throw an error if you don't include this check. Nobody likes errors.
Now we have a class on our .button--configuration container, so let's see how we can use this in Sass. Foundation has a mixin for button style, but you could easily roll your own.
// file: _paragraphs--button.scss
.button--configuration {
a {
@include button; // this is the Foundation mixin for creating a button.
}
// loop over our colors to look for our button-color-- class
@each $color-name, $color-value in $color-palette {
&.button-color--#{$color-name} {
// @include button-style($background, $background-hover, $color);
@include button-style($color-name, auto, auto);
}
}
}
If you notice, the button-style mixin has two more parameters $background-hover, and $color. For this post, I just set those to 'auto', but if we wanted to give authors control over those settings, we would simply add more "Configuration" fields to our Button bundle, preprocess them, and include in our Sass.
Now, you can go crazy adding configuration fields to each paragraph bundle in an effort to give authors the maximum control over the site, but it gets time consuming and makes for a crazy and tedious author experience. It's best to use some judgement, think about what the author actually needs, keep things organized, and set sensible defaults so the author doesn't have to touch every field ever time. One way to help with the organization is to use the Field Group module. Usually, I create a Details group, name it something like "Button Configuration", and put all of my Configuration fields in there. If there are a lot of configuration fields, I will group them in tabs.
This is an example of how a Button paragraph form display might look. The configuration is all tucked away in a details group above the actual content field, with tabs for each of the settings.
Button Configuration ( Details, collapsed )
- Configuration Tabs ( Tabs )
- Background Color ( Tab )
- Button Background Color ( field_button_background_color, select list )
- Text Color ( Tab )
- Button Text Color ( field_button_text_color, select list )
Button ( field_button, link field )
A bit about Page Layout
Hopefully I have illustrated how you can use Paragraphs to give authors control over individual components. This an awesome thing. But now we want to give authors more control over the whole page.
Bootstrap and older versions of Foundation had the concept of "Rows" and "Columns". These basically defined a context for laying out content horizontally, also known as a "Grid Layout". A "Row" is a container, and a "Column" defines how much horizontal space some chunk of content can use. The current version of Foundation ( 6.5.3 at this time ) uses a concept they call XY Grids, very similar to traditional grids, only leveraging CSS Flexbox, which is awesome. Instead of "Rows" and "Columns", there are "Grid-X" ( horizontal layouts ) or "Grid-Y" ( vertical layouts ), and "Cells". I wouldn't call Cells "Columns", because if used in a vertical layout context, they define vertical space, rather than horizontal. However, it's easier to think about Cells in the horizontal context as Columns, and that is probably your primary concern, how to layout out the horizontal aspects of the page.
To create a grid layout, there are a series of containers, the outer most establishes the context ( Grid X ), the inner defines the space for the content ( Cell ). Here is an example of how the HTML must be structured to leverage the XY Grid.
<div class="grid-x">
<div class="cell">
<!-- The first column Content Here -->
</div>
<div class="cell">
<!-- The second column Content Here -->
</div>
</div>
This creates two Cells or columns. We can set the sizes of those Cells by adding classes, prefixed by the size screen we want to target. By default Foundation uses a 12 column grid, so we could do something like:
<div class="grid-x">
<div class="cell small-12 medium-6 large-4">
<!-- The first column Content Here -->
</div>
<div class="cell small-12 medium-6 large-8">
<!-- The second column Content Here -->
</div>
</div>
Again, this is not a tutorial on Foundation, so read up.
The real question is, how do we implement something like this in a way that an author can control? Remember, we want to minimize the amount of knowledge the author needs about how CSS works, they just want to select some options and the page look as they expect.
Paragraphs for Page Layout
One of the coolest aspect of Paragraphs is the ability to "nest" paragraph bundles. That is, a Paragraph bundle that has another Paragraph bundle as it's content field. We can leverage this nested approach to create the kind of complex structure an author probably expects of his shiny CMS. To do this, we're going to build "inside out". Hopefully you will see what I mean.
The first step would be: remove all fields from our Basic Page content type. There should only be a title ( it's a Drupal requirement anyway ). We want the Basic Page to be a clean slate for our authors.
We already have our Paragraph "component" bundles, Text, Image, Title, Button. You can add as many as you want, but I encourage you to keep them relatively simple, only one content field per bundle, otherwise, the author may have difficulty understanding what they should choose.
Now, let's create a new Paragraph bundle, you can call it Cell. The Cell is the container for our components, so we need to add one field, and Entity Reference Revision -> Paragraph, call it Components ( field_components ). In the field storage settings, set to Unlimited. In the field settings, choose each of the component bundles you want your author to be able to add to the page.
Next, add some fields (of type Text List ) for the setting columns. I would typically add a field for each of my screen sizes, field_small_size, field_medium_size, field_large_size.
field_small_size
small-12|100%
small-6|50%
field_medium_size
medium-12|100%
medium-9|75%
medium-8|66%
medium-6|50%
medium-4|33%
medium-3|25%
field_large_size
large-12|100%
large-9|75%
large-8|66%
large-6|50%
large-4|33%
large-3|25%
Notice, I didn't give the author every possible grid size, I can't think of a case where you would want small-1. Also, I display a percentage in the select options rather than the cell size. The cell sizes are implementation details authors don't understand. They understand percents. This is also a great time to add some assistance, in the form of some help text for the author. I like the Markup module for adding instructions. Drupal has Help text area for each field, but it's displayed below the field. This can clutter the UI, and the one thing I learned in Technical Writing is ALWAYS INCLUDE THE WARNING ( and instruction ) BEFORE THE ACTION. Thanks Dr. Wiseman! Just be sure to remove this from the Display or visitors will see your help text!
As before, I am going to put these settings in field group tabs to minimize the clutter.
Next, we need to add add a preprocess function in our theme.
<?php
// file: THEME.theme
/**
* Implements hook_preprocess_paragraph__type().
*/
function THEME_preprocess_paragraph__cell(array &$variables) {
$paragraph = $variables['paragraph'];
$variables['attributes']['class'][] = 'cell';
if ($paragraph->hasField('field_small_size') && !$paragraph->field_small_size->isEmpty()) {
$variables['attributes']['class'][] = $paragraph->field_small_size->value;
}
if ($paragraph->hasField('field_medium_size') && !$paragraph->field_medium_size->isEmpty()) {
$variables['attributes']['class'][] = $paragraph->field_medium_size->value;
}
if ($paragraph->hasField('field_large_size') && !$paragraph->field_large_size->isEmpty()) {
$variables['attributes']['class'][] = $paragraph->field_large_size->value;
}
}
Now we have our Cells, which contain our components, and the author can control the horizontal space at 3 different screen sizes. That is some powerful stuff.
We could just add our Cell field to our Basic Page content type and call it a day, but we'd be missing a few things.
For one, Cells need to have a container with the class .grid-x . We could easily use the Fences module on the Basic Page -> Manage Display to add that class to the Cell field. The result would be that our layout would always be the full-width of our screen. My experience is, full-width content is difficult to make look good for all content. It also means authors can't do things like add a background color to a row. What I find is, for the majority of content, you want it contained to a center area ( or left or right ) of the page, with only some content able to be full width ( like Hero images ).
Creating Full-Width and Contained Sections
Let's say our page layout consists of a full-width Hero image, and below that 3 columns, centered, each with an image, a title and some text with a button. How would we give authors that sort of capability?
Here is what the markup would need to look like:
<div class="grid-x">
<div class="cell small-12 medium-12 large-12">
<img> <!-- our hero image -->
</div>
</div>
<div class="grid-container"> <!-- need this wrapper -->
<div class="grid-x">
<div class="cell small-12 medium-4 large-4">
<img>
<h2>Title 1</h2>
<p>A bit of text</p>
</div>
<div class="cell small-12 medium-4 large-4">
<img>
<h2>Title 2</h2>
<p>A bit of text</p>
</div>
<div class="cell small-12 medium-4 large-4">
<img>
<h2>Title 3</h2>
<p>A bit of text</p>
</div>
</div>
</div>
If you didn't notice, there is a wrapper element around our second .grid-x, .grid-container . This is a Foundation feature that will contain the contents of the .grid-x to a predefined center region. This is all configurable through SASS variables, but the defaults are fine for this discussion. The point is, we need the author to be able to decide if the content will be the full-width of the screen or contained to the center region with minimum fuss.
For starters, if you already added a Cell field to the Basic Page, delete it.
Next, let's add a new Paragraph bundle. call it Section. Add an Entity Reference Revision field, Cell, Unlimited, and reference the Cell bundle.
Now you can add a boolean field, Contained. I would have it checked by default.
Previously, we were using preprocess functions to add a class to our paragraph field. Now we need to actually change our markup. I start with the preprocess function in my THEME.theme file.
<?php
// file: THEME.theme
/**
* Implements HOOK_preprocess_paragraph__type().
*/
function THEME_preprocess_paragraph__section(array &$variables) {
$paragraph = $variables['paragraph'];
if ($paragraph->hasField('field_contained') && !$paragraph->field_contained->isEmpty() && $paragraph->field_contained->value == TRUE) {
$variables['contained'] = 'TRUE';
}
else {
$variables['contained'] = 'FALSE';
}
You notice that this time, I didn't add a class, I created an entirely new variable, $variables['contained'], and I gave it a string, "TRUE" or "FALSE". I've had some challenges with Twig and truthiness, so, string comparisons it is.
If you've enabled Twig debug on your Drupal site, you will see the theme suggestion for our paragraph includes something like 'paragraph--section.html.twig'. More on Twig debug mode. The gist is, copy paragraph.html.twig from the Paragraphs module directory into your theme template directory.
themes/custom/THEME/templates/paragraph--section.html.twig
Next edit the template so it looks something like this:
{# file: paragraph--section.html.twig #}
{%
set classes = [
'paragraph',
'paragraph--type--' ~ paragraph.bundle|clean_class,
view_mode ? 'paragraph--view-mode--' ~ view_mode|clean_class,
not paragraph.isPublished() ? 'paragraph--unpublished'
]
%}
{% block paragraph %}
<div{{ attributes.addClass(classes) }}>
{% if contained == 'TRUE' %}
<div class="grid-container">
{% endif %}
<div class="grid-x">
{% block content %}
{{ content }}
{% endblock %}
</div>
{% if contained == 'TRUE' %}
</div>
{% endif %}
</div>
There are a couple of things going on here.
- It checks that our variable 'contained' == 'TRUE'
- If yes, adds a div.grid-container
- Add the container div.grid-x
Be sure to clear cache after adding a new template.
Now our author can control which sections are full-width and which are contained by checking a box. Huzzah!
Go ahead and add the Entity Reference Revisions paragraph field "Sections" ( referencing Sections bundle ) to your content type and you are almost done.
Something you will likely notice is, the Cells don't automatically work. This is because of the extra markup Drupal injects around fields.
There are many ways to deal with this, I find the simplest is to install the Fences module. Go to Admin->Structure->Paragraphs->Sections->Manage Display. Click the gear to the right of the field Cell, and set all the wrappers to None. Now the Cells should use the grid as intended.
Finally, the Author Experience
Now that we're all set up, here is how the author would build a page:
- Visit /node/add/page.
- Click Add Section -> Configure the Section ( Contained, and whatever other settings you added ).
- Click Add Cell -> Configure the Cell width for each screen size.
- Click Add Image / Add Title / Add Text / Add Button from the Components drop button and configure to taste.
There are a lot of other options I like to give authors, like background colors on sections, background images ( checkout Field Group Background Image ), padding options. Just try to keep the UI sensible, and use language a human being would understand.
If authors like Legos ( and who doesn't? ), they should quickly take to building pages with Paragraphs. As always, they may need some help getting their heads around the basic concepts, so provide excellent help. It could be a video, images, etc, using the Markup field is great for this. Just keep in mind, images added directly through the Markup field editor get wiped out. It's a weird Drupal thing, that is easy to work around by uploading your images to the theme or somewhere else.