Custom Block Type for Hero banners in Drupal 8

Create a re-usable Custom Block Type to make it easy for site-builders and content-editors to make hero banners.

What we’ll be making

A custom block for hero banners. Make as many as you like, edit like a node form, and place as you’d place any block.

A Hero banner custom block with a title, body, link, and background image.
They’re created by just a block form, so no coding for content-editors.

What you’ll need

  • Drupal 8
  • Access to your theme files.
  • My theme is built with a CSS framework (Bootstrap 4), but this is not required.
  • Basic understanding of HTML and CSS and a little Twig. I will provide code snippets.

1. Create a Hero custom block type

Custom block types allow you to expand on the regular custom block, which only has a title and body. We can create all your own fields and manage the display almost like a regular content type. We can also create custom theme templates that pull the fields into custom HTML.

  1. Structure > Block Layout > Custom Block Library tab > Block Types tab or admin/structure/block/block-content/types
  2. ‘Add custom block type’
Custom Block Types UI

3. I named my block type ‘Hero’, and my description is: Large banner with a background image, title, text area, and button.

4. Add fields:

  • Image field called Background Image
  • Link field called Button
My fields for the Hero custom block type

5. I re-order the fields: Block Description, Body, Button and Background Image.

2. Create a theme template file

Drupal 8’s Twig debugging tool helps me find out what to name my theme template.

  1. Enable Twig debugging.
  2. Create a test Hero custom block and place it in a region. It will look very icky by default.
My test block by default…ew

3. Use browser dev tools to see theme template suggestions. Oh no! I see template suggestions based on block type fields, and the block name, but not a block-level template for block types!

<!-- FILE NAME SUGGESTIONS:
* block--loomings.html.twig
* block--block-content--75948f7b-436b-4bed-9111-094b49fbf5b7.html.twig
* block--block-content.html.twig
* block--block-content.html.twig
x block.html.twig
-->
<!-- FILE NAME SUGGESTIONS:
* field--block-content--body--hero.html.twig
* field--block-content--body.html.twig
* field--block-content--hero.html.twig
* field--body.html.twig
* field--text-with-summary.html.twig
x field.html.twig
-->

That’s no good. But check it: we can add a pre-process function to our theme’s .theme file as suggested by Jeff Burnz in this Drupal.org support thread. I just add this to the bottom of the .theme file in my theme’s root folder and replace THEMENAME with my theme name. 🐬

/**
* Implements hook_theme_suggestions_HOOK_alter() for form templates.
* @param array $suggestions
* @param array $variables
*/
function THEMENAME_theme_suggestions_block_alter(array &$suggestions, array $variables) {
// Block suggestions for custom block bundles.
if (isset($variables['elements']['content']['#block_content'])) {
array_splice($suggestions, 1, 0, 'block__bundle__' . $variables['elements']['content']['#block_content']->bundle());
}
}

Now, when I clear cache and re-inspect my custom block, I see this:

<!-- FILE NAME SUGGESTIONS:
* block--loomings..html.twig
* block--block-content--75948f7b-436b-4bed-9111-094b49fbf5b7.html.twig
* block--block-content.html.twig
* block--bundle--hero.html.twig
* block--block-content.html.twig
x block.html.twig
-->

I see that this file overrides block.html.twig so I’ll make a copy of that and put it in my templates folder.

theme folder
- templates
-- block--bundle--hero.html.twig

3. Code my hero block template

This is the most default block.html.twig:

<div{{ attributes }}>
{{ title_prefix }}
{% if label %}
<h2{{ title_attributes }}>{{ label }}</h2>
{% endif %}
{{ title_suffix }}
{% block content %}
{{ content }}
{% endblock %}
</div>

I want to use the principles I see in that file to wrap things in my own HTML , and pull in specific fields.

I always start with the HTML, no Twig. I will use the Bootstrap 4 ‘jumbotron’ component with some modifications, like columns and inline CSS for the background image. You can certainly use a different framework, or code up your own.

<div class="jumbotron jumbotron-fluid" style="background-image: url(MYIMAGE); background-size: cover; background-position: center" >
<div class="container">
<div class="row">
<div class="col-6">
<h2 class="display-4">Title</h2>
<div class="lead">Body</div>
<div><a href="#" class="btn btn-primary">Button Text</a></div>
</div>
</div>
</div>
</div>

How to pull in specific fields

I will post the whole code snippet at the end of this section.

EDIT: I wrote a guide on how to work with parts of fields in Drupal 8 twig templates: https://medium.com/@sarahcodes/getting-drupal-8-field-values-in-twig-22b80cb609bd

Block title

Easy enough — I can just copy what they have, and add my classes.

Body field

The machine name of the Body field is body. So I use {{ content.body }}.

Button

Button is a Whole-Ass Link. {{ content.field_button }} is going to bring in the <a> element in its entirety, but I want to add my CSS framework class to it. If you don’t need to add a class to the link, just use {{ content.field_button }} and move on with your life. Else, here’s how to get the url and label separately:

<a href="{{ content.field_button[0]['#url'] }}" class="btn btn-primary">{{ content.field_button[0]['#title'] }}</a>

Background Image

I just need the url of the image. If I just use {{ content.field_background_image }} I get the Whole-Ass Image. I need to get tricky to dig out the actual url. Here’s where I found that solution on Drupal StackExchange.

{{ file_url(content.field_background_image['#items'].entity.uri.value) }}

NOTE: If you still have Twig debugging on, this will break *like crazy*. So you can turn it off now.

The code

<div class="jumbotron jumbotron-fluid" style="background-image: url({{ file_url(content.field_background_image['#items'].entity.uri.value) }}); background-size: cover; background-position: center" >
<div class="container">
<div class="row">
<div class="col-md-6">
{{ title_prefix }}
{% if label %}

<h2{{ title_attributes.addClass('display-4') }}>{{ label }}</h2>
{% endif %}
{{ title_suffix }}

<div class="lead">{{ content.body }}</div>
<div><a href="{{ content.field_button[0]['#url'] }}" class="btn btn-primary">{{ content.field_button[0]['#title'] }}</a></div>
</div>
</div>
</div>
</div>

4. Accessibility Concerns

When designing and building your Hero, keep in mind some basic digital accessibility concerns.

  1. Text/background contrast. Text should have sufficient color contrast from the background so it’s easier for more people to read. If you intend to always use a background image (like my example), I recommend altering the template and CSS so the caption area always has a solid background. (My example does *not* have sufficient contrast, so I’d need to work on that for production.) Here’s a free color contrast checker.
  2. Semantic HTML. Use the correct HTML elements and attributes to convey the intended meaning. I’m still looking into what would be a clear way to mark-up a banner element so that it makes sense in most contexts. I’m not sure if that’ article or aside, and if that allows us to use a self-contained h1 or not. Hmm…

Conclusion

I think that does it. Read on for some advanced techniques.


Advanced

Conditional statements

If any of the fields are optional, it will be cleaner and less-broken if we add conditional statements to the template to check if the field is filled before rendering it. We already have that for the label/title, which can be toggled on and off in the Block’s configuration.

Here’s the code again, with the conditional statements:

<div{{ attributes }}>
<div class="jumbotron jumbotron-fluid" {% if content.field_background_image['#items'].entity.uri.value %} style="background-image: url({{ file_url(content.field_background_image['#items'].entity.uri.value) }}); background-size: cover; background-position: center"{% endif %} >
<div class="container">
<div class="row">
<div class="col-md-6">
{{ title_prefix }}
{% if label %}
<h2{{ title_attributes.addClass('display-4') }}>{{ label }}</h2>
{% endif %}
{{ title_suffix }}
{% if content.body is not empty %}
<div class="lead">{{ content.body }}</div>
{% endif %}
{% if content.field_button[0]['#url'] %}
<div><a href="{{ content.field_button[0]['#url'] }}" class="btn btn-primary">{{ content.field_button[0]['#title'] }}</a></div>
{% endif %}
</div>
</div>
</div>
</div>
</div>

Custom Options

Fields can be used to add options or settings to the custom block. Example, I might like the option to make my content area contained or full-width.

In this example, I created a List(text) field with two options, container|Contained and container-fluid|Full (the keys, in bold, are classnames from Bootstrap 4). Now, I can use the field value in the template as a class. To get the super raw value of a select list…

{{ content.field_width[0][‘#markup’] }}

<div{{ attributes }}>
<div class="jumbotron jumbotron-fluid" {% if content.field_background_image['#items'].entity.uri.value %} style="background-image: url({{ file_url(content.field_background_image['#items'].entity.uri.value) }}); background-size: cover; background-position: center"{% endif %} >
<div class="{{ content.field_width[0]['#markup'] }}">
<div class="row">
<div class="col-md-6">
{{ title_prefix }}
{% if label %}
<h2{{ title_attributes.addClass('display-4') }}>{{ label }}</h2>
{% endif %}
{{ title_suffix }}
{% if content.body is not empty %}
<div class="lead">{{ content.body }}</div>
{% endif %}
{% if content.field_button[0]['#url'] %}
<div><a href="{{ content.field_button[0]['#url'] }}" class="btn btn-primary">{{ content.field_button[0]['#title'] }}</a></div>
{% endif %}
</div>
</div>
</div>
</div>
</div>

Okay, that’s it. I’ll update if I come up with any other cool things to do with custom blocks.