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.
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.
- Structure > Block Layout > Custom Block Library tab > Block Types tab or
admin/structure/block/block-content/types
- ‘Add custom block type’
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
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.
- Enable Twig debugging.
- Create a test Hero custom block and place it in a region. It will look very icky by default.
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.
- 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.
- 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
oraside
, and if that allows us to use a self-containedh1
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.