Custom block type for a Stat Card in Drupal 8

Sarah Carney
6 min readApr 15, 2021

Here is how I created a simple custom block type in Drupal 8 to highlight a statistic. I’m going to take it step-by-step, provide all code snippets, and explain what the code means.

Screenshot of three ‘Stat Cards’. Each one shows an icon, large number or value, caption, and button.

Steps

  1. Write the HTML and CSS for the block design
  2. Create a custom block type and fields
  3. Identify the twig template suggestion
  4. Mesh the HTML with Twig

Notes: My exact implementation requires the theme has Bootstrap 4 and Font Awesome 4 but that’s not required to apply these principles.

Write HTML and CSS for block design

My first step for creating components is always writing the HTML/CSS/design which guides me in breaking down the dynamic pieces (fields).

Here is my HTML:

<div class="stat-card">
<div class="stat-card_icon">
<span class="fa fa-ICON-NAME"></span>
</div>
<div class="stat-card_primary">PRIMARY</div>
<div class="stat-card_secondary">SECONDARY</div>
<div class="stat-card_button">
<a href="URL" class="btn btn-danger">TEXT</a>
</div>
</div>
  • The bold items will be filled in by the field.
  • I am going to use FontAwesome 4 icons.
  • Our theme uses the Bootstrap 4 CSS framework, so we can style our button in the HTML with the btn and btn-danger classes.

Here is my CSS:

.stat-card {
margin: 0 auto;
text-align: center;
max-width: 500px;
}
.stat-card_icon {
font-size: 5rem;
line-height: 1.25;
text-shadow: 3px 3px #cccccc;
}
.stat-card_primary {
font-size: 5rem;
line-height: 1.25;
font-weight: bold;
text-shadow: 3px 3px #cccccc;
}
.stat-card_secondary {
font-size: 1.5rem;
}

Create a custom block type and add fields

  1. Structure > Block Layout > Custom Blocks > Custom Block Types > Add Custom Block Type
  2. I used the label ‘Stat Card’ so my machine name to note later is stat_card.
  3. Begin adding fields

Icon (field_stat_card_icon)
Field type: Text (plain)

Primary Text (field_stat_card_primary)
Field type: Text (plain)

Secondary Text (field_stat_card_secondary)
Field type: Text (formatted, long)
Text format: We have a text format called Simple WYSIWYG which allows only bold, italic, special symbols and links.

Button (field_stat_card_button)
Field type: Link
Allow link text: Required

Now we need to handle a few things in the block type’s Manage Display. The order does not matter.

  • I will hide the Label for each field
  • Icon and Primary Text — Format: Plain text
  • Secondary Text — Format: Default
  • Button — Format: Separate link text and URL
Screenshot of the Manage Display settings for the fields. This information was typed about above.

Identify the Twig template suggestion

Theme templates are where we combine HTML and Twig to create the custom block type.

Template suggestions are the name of the template file that Drupal will recognize. They are suggested from general to specific. We want to choose the template that will style a custom block type. This requires two steps.

Tell Drupal to make theme suggestions for custom block types in my_theme.theme

function my_theme_theme_suggestions_block_alter(&$suggestions, $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());
}
}

Identify the template name

This means my template suggestion for stat_card will be:

block--bundle--stat-card.html.twig

Note that the underscore in stat_card becomes a dash in the template file name.

Create a file with that name and put it in your theme’s templates directory.

Mesh the HTML and Twig to write the template

There are two main steps to making a theme template that works well.

  1. Include all the required extra code so the block accepts any default Drupal settings.
  2. Fill in your HTML and Twig

I have bolded our original HTML.

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

{% block content %}
{# Helpful for making sure that edits to the block update without a cache clear #}
{{ content|without('field_stat_card_icon', 'field_stat_card_image','field_stat_card_primary', 'field_stat_card_secondary', 'field_stat_card_button') }}
<div class="stat-card">
{% if content.field_stat_card_icon[0] is not empty %}
<div class="stat-card_icon">
<span class="fa fa-{{ content.field_stat_card_icon[0] }}"></span>
</div>

{% endif %}
{% if content.field_stat_card_primary[0] is not empty %}
<div class="stat-card_primary">
{{ content.field_stat_card_primary }}
</div>
{% endif %}
{% if content.field_stat_card_secondary[0] is not empty %}
<div class="stat-card_secondary">
{{ content.field_stat_card_secondary }}
</div>
{% endif %}
{% if content.field_stat_card_button[0] is not empty %}
<div class="stat-card_button">
{% if content.field_stat_card_button.0['#url'].toString() is empty %}
<span class="btn btn-danger">
{% else %}
<a href="{{ content.field_stat_card_button[0]['#url'] }}" class="btn btn-danger">
{% endif %}
{{ content.field_stat_card_button[0]['#title'] }}
{% if content.field_stat_card_button.0['#url'].toString() is empty %}
</span>
{% else %}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
</div>

Here is that same code again with the Twig logic and field values highlighted:

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

{% block content %}
{# Helpful for making sure that edits to the block update without a cache clear #}
{{ content|without('field_stat_card_icon', 'field_stat_card_image','field_stat_card_primary', 'field_stat_card_secondary', 'field_stat_card_button') }}
<div class="stat-card">
{% if content.field_stat_card_icon[0] is not empty %}
<div class="stat-card_icon">
<span class="fa fa-{{ content.field_stat_card_icon[0] }}"></span>
</div>
{% endif %}
{% if content.field_stat_card_primary[0] is not empty %}

<div class="stat-card_primary">
{{ content.field_stat_card_primary }}
</div>
{% endif %}
{% if content.field_stat_card_secondary[0] is not empty %}

<div class="stat-card_secondary">
{{ content.field_stat_card_secondary }}
</div>
{% endif %}
{% if content.field_stat_card_button[0] is not empty %}

<div class="stat-card_button">
{% if content.field_stat_card_button.0['#url'].toString() is empty %}
<span class="btn btn-danger">
{% else %}
<a href="{{ content.field_stat_card_button[0]['#url'] }}" class="btn btn-danger">
{% endif %}
{{ content.field_stat_card_button[0]['#title'] }}
{% if content.field_stat_card_button.0['#url'].toString() is empty %}

</span>
{% else %}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}
</div>

Here are some concepts demonstrated in this template

Field values

This is the stuff between the double curly brackets. {{ … }} Note how the machine names are used.

In some cases, I’m just getting exactly what’s configured in Manage Display such as {{ content.field_stat_card_primary }}.

In other cases, I need to pick apart the field’s value’s parts such as {{ content.field_stat_card_button[0][‘#url’] }}. I only want the button’s url because I want to make my own HTML for the button.

Here’s more about how to show different parts of Drupal 8 field values in twig.

How the icon field works

Here is the help text on. my icon field

Enter the name of a FontAwesome icon. Example: arrow-right. All FontAwesome4 icons.

The HTML is only asking for the icon name so that’s all that needs to be entered in the field.

<span class="fa fa-ICON-NAME"></span>

Entering arrow-right into the field produces this HTML which is what FontAwesome needs to display an icon.

<span class="fa fa-arrow-right"></span>

if/else/endif

These lines check to see if a condition is true before showing something. I don’t want the HTML of a part if the field is not filled out.

Accounting for <none> in the button URL

It would be a bad idea for someone to enter <none> in the URL, but if it happens, this code makes sure there isn’t a link to nowhere.

{% if content.field_stat_card_button.0['#url'].toString() is empty %}

What is the part about clearing the cache?

My colleague found the solution to a bug we found that sometimes the block wouldn’t update after editing without a cache clear. This seems to solve that.

{# Helpful for making sure that edits to the block update without a cache clear #}
{{ content|without('field_stat_card_icon', 'field_stat_card_image','field_stat_card_primary', 'field_stat_card_secondary', 'field_stat_card_button') }}

Required code for a block template

This template overrides Drupal’s default block template so we must include some code to make sure it works right.

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

{% block content %}
...
{% endblock %}
</div>

Okay done!

Hope you can apply the techniques in this tutorial in your own blocks.

--

--