Web Development

Drupal 8 theme: How to develop a custom theme.

Drupal 8 Logo

Whatever your use case is for developing a custom theme in Drupal 8, the PHP Symfony, Twig, and markup goodness Drupal 8 offers developers is great. Let’s cut to the chase and get started.

You can easily scaffold a theme using drupal console with the drupal generate:theme [options] command found at https://hechoendrupal.gitbooks.io/drupal-console/en/commands/generate-theme.html. In this guide, we will be creating a custom Drupal 8, manually generating the files as they are needed. However, do not overlook this command for future use. After you go through this guide and understand what composes a theme, I recommend for future projects using this command.

Create a Drupal 8 theme directory.

The first thing you will need to do is create a directory for your theme. Generally speaking, best practice is to create the directory structure as follows: docroot/themes/custom/YOURTHEMENAME

In this example, we will name our theme directory as docroot/themes/custom/artistCamp

A quick note on yml files.

Anytime you see indentation in yml files, the indentation are important and are only 2 spaces. That is to say, if something is indented twice, it equates to 4 spaces. Improper indentation of yml files will make things not work properly. And generally speaking, improper syntax at all will break things.

*.info.yml

This file is required. Once you have your theme directory created, you will need to create the *.info.yml file. In our case, we will name this file artistCamp.info.yml. This file is a configuration file which will register our theme with our Drupal 8 install. In this file there are a few required key/value pairs as well as many optional key/value pairs that we define as metadata to describe our theme.

Required *.info.yml key/value pairs.

The required keys are name, type, and core. For our example we will define a few other useful keys. Below you will see the contents of artistCamp.info.yml.

*.info.yml file used for registering your Drupal 8 theme with the system.

Some of the keys are self-evident such as “description” but let’s elaborate on a few of the other keys.

base theme: Your theme can derive from another theme and according to drupal.org:

“Sub-themes are just like any other theme, with one difference: they inherit the parent theme’s resources. There are no limits on the chaining capabilities connecting sub-themes to their parents. A sub-theme can be a child of another sub-theme, and it can be branched and organized however you see fit. This is what gives sub-themes great potential.”

screenshot: The “Appearance” admin user-interface (/admin/appearance), when you are selecting a theme to use for your website, will show the image you specify via the path provided to the screenshot key.

regions: This is very important metadata that creates reference-able regions that will become the main parts/sections of your theme. Each region renders as its own encapsulated black box. Regions are reference-able via their unique hook. These hooks are essentially IDs that reference your regions for theming and pre-processing. More on this later. The Structure -> Block Layout admin user-interface displays the custom regions you define, so that administrators can add blocks to those regions as needed.

libraries: A reference to a defined Drupal 8 “library” (that is defined in *.libraries.yml more on this later) applied to every node (in Drupal jargin a node is a page) using this theme.

*.theme

Include a theme as it is best practice to have one. If you create it blank, make sure to at least include the <?php at the top of the file, and including a comment like /* Stub */ may be nice as well.

This file allows you to implement a list of life-cycle hook functions associated with your theme such as HOOK_preprocess_page. There are a plethora of contextual hooks you can implement, of which you can check out on the drupal.org website. However, for this file, if we were to implement HOOK_preprocess_page for the artistCamp example, the function name would be, artistCamp_preprocess_page. This function allows us to prepare variables for the page.html.twig file. Drupal inherently knows when to run this function. The equivocation of this function prepares our variables for the page.html.twig template to use. Drupal 8 accomplishes this with its event system runs implemented life-cycle hooks at the proper times.

Preprocessing Drupal 8 theme variables.

As we saw in page.html.twig, our Twig templates can be passed preprocessed variables. That is to say, any Twig template can have custom, defined variables at their disposal if we set them up properly, one way being through preprocess functions. Let’s take a look at the artistCamp_preprocess_page function defined in the artistCamp.theme file.

hook_preprocess_page function used for preparing variables for use in your Drupal 8 theme twig templates.

It is worth noting before commenting on the above function definition that to create custom variables for use in Twig templates via their preprocess function counter-part, that you create them in the following format:

$variables['foo'] = "bar";

So, we create a custom variable called ‘current_route’ to use in the artistCamp page.html.twig template. The route the current user is requesting becomes the value of ‘current_route’. This function only runs on nodes that use the page.html.twig template, which, for most sites, will be used on every page.

Next we will look at the html.html.twig file, however I would urge you quickly scroll past it and look at the page.html.twig file to fully follow this example. To clarify, the ‘current_route’ variable determines the markup generated to the client requesting the web page. I recommend to still read the html.html.twig section.

html.html.twig

This twig template serves as the entry point for ultimately rendering all of your templates. It always runs on page loads that are uncached. Let’s take a look at the artistCamp html.html.twig file.

<?php
{#
/**
* @author Brad Mash, Software Engineer
* @file
* Theme override for the basic structure of a single Drupal page.
*
* Variables:
* - logged_in: A flag indicating if user is logged in.
* - root_path: The root path of the current page (e.g., node, admin, user).
* - node_type: The content type for the current node, if the page is a node.
* - head_title: List of text elements that make up the head_title variable.
*   May contain one or more of the following:
*   - title: The title of the page.
*   - name: The name of the site.
*   - slogan: The slogan of the site.
* - page_top: Initial rendered markup. This should be printed before 'page'.
* - page: The rendered page markup.
* - page_bottom: Closing rendered markup. This variable should be printed after
*   'page'.
* - db_offline: A flag indicating if the database is offline.
* - placeholder_token: The token for generating head, css, js and js-bottom
*   placeholders.
*
* @see template_preprocess_html()
*/
#}
{%
set body_classes = [
logged_in ? 'user-logged-in',
not root_path ? 'path-frontpage' : 'path-' ~ root_path|clean_class,
node_type ? 'page-node-type-' ~ node_type|clean_class,
db_offline ? 'db-offline',
'no-js'
]
%}

{{attach_library('artistCamp/global-header')}}
{{attach_library('artistCamp/global-footer')}}

{% set frontClass = ""  %}
{% if current_route == "view.frontpage.page_1"  %}
{% set frontClass = "frontpage" %}
{% endif %}

<!DOCTYPE html>
<html{{ html_attributes.setAttribute('class',frontClass) }}>
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', 'UA-XXXXXXXXX');
</script>

{#
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXXX');</script>
<!-- End Google Tag Manager -->


<link rel="preload" as="script" href="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js">
<script>
(adsbygoogle = window.adsbygoogle || []).push({
google_ad_client: "ca-pub-XXXXXXXXXXXXX",
enable_page_level_ads: true
});
</script>
#}
<head-placeholder token="{{ placeholder_token }}">
<title>{{ head_title|safe_join(' | ')|trim('| ') }}</title>
<css-placeholder token="{{ placeholder_token }}">
<js-placeholder token="{{ placeholder_token }}">
</head>
<body{{ attributes.addClass(body_classes) }}>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->

{#
<section aria-label="Skip to main content.">
#}
{# Using the open source implementation found at: https://accessibility.oit.ncsu.edu/it-accessibility-at-nc-state/developers/accessibility-handbook/mouse-and-keyboard-events/skip-to-main-content/ #}
{#
<a href="#page-instance-fluid" class="skip-main">
{{ 'Skip to main content'|t }}
</a>
</section>
#}
{# Do not remove these three: page_top; page; page_bottom; ever! #}
{{ page_top }}
{{ page }}
{{ page_bottom }}
<js-bottom-placeholder token="{{ placeholder_token }}">
</body>
</html>

You can see that the main HTML elements are here, html, head, body.

Rather than go into great detail about everything going on in this file, we will comment on some high-level things. First notice that we can attach libraries directly to our template with a single twig function:

{{attach_library(‘artistCamp/global-header’)}}

Libraries will be discussed in the *.libraries.yml section below. However, what this is doing is taking all of the resources listed in the ‘artistCamp/global-header’ library (mainly CSS and JavaScript references), packaging them up, and applying them to the final rendered page markup this template produces.

Finally, towards the bottom of the file you will notice the following:

{{ page_top }}
{{ page }}
{{ page_bottom }}

These are variables which reference global parent regions (not the regions we defined, these regions, particularly the page region, hold our custom defined regions we defined in our *.info.yml file). If you’re curious why page_top and page_bottom are not accessible in Block Layout you can see the following link, https://api.drupal.org/api/drupal/core!modules!system!system.module/function/system_system_info_alter/8.2.x. But in short, these are system administered regions.

page.html.twig

If you’re following, you should see that whatever is output via this file will be between the <body>…</body> tag since {{ page}} was output there as we saw when we looked at the html.html.twig file.

This is the default template that all themes use if no other twig template overrides their precedence (more on twig hook theme precedence later). To clarify, if your theme includes all of the files this tutorial has outlined up to this point, as well as this file, this is the file that is basically going to generate all of the markup for your website no matter what page you’re on.

Let’s take a look at the artistCamp page.html.twig file, and keep in mind that we preprocessed this twig template via out *.theme file’s artistCamp_preprocess_page function which made the ‘current_route’ variable available.

<?php
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
{#
    /**
* @author Brad Mash, Software Engineer
* @file
* Artist Camp Theme File
* page.html.twig
* Implements Bootstrap 4.1.x
*/
    #}
    <!-- Artist Camp Developed by Brad Mash. -->
    <div id="global-container" class="global-container">
{% include directory ~ '/templates/includes/header/header.html.twig' %}
<div id="global-content-container">
<div id="content" class="content">
<div class="top-fluid-container">
<div id="top-fluid" class="top-fluid">
{{ page.top_fluid }}
</div>
</div>

{% set containerized_routes = ['user.login', 'user.register', 'node.add','node.add_page','entity.user.edit_form','comment.admin','view.files.page_1','view.message.page_1']  %}
<div id="page-instance-fluid" class="page-instance-fluid">
<main id="main">
{% if current_route in containerized_routes %}
<div class="container">
{{ page.content }}
</div>
{% else %}
{{ page.content }}
{% endif %}
</main>
</div>

<div class="bottom-fluid-container">
<div id="bottom-fluid" class="bottom-fluid">
{{ page.bottom_fluid }}
</div>
</div>
{% include directory ~ '/templates/includes/footer/footer.html.twig' %}
</div>
</div>

{# {% include directory ~ '/templates/includes/footer/footer.html.twig' %} #}
    </div>

Again, rather than comment on every part of the file, let’s consider the more interesting parts. The first thing we want to point out is the use of our custom defined regions such as {{ page.top_fluid }}

If needed can theme the top_fluid region with a twig template. We will not spend time doing this in this tutorial. However, Drupal will provide theme hooks for this region which can be themed and preprocessed as needed.

**Note: This may be a good point in time to say that twig templating in Drupal 8 is very granular in scope. For example, a form on a page can be twig templated and preprocessed. But then, each input of the form themselves can be as well! Twig templating is a game of inception.

Drupal 8 Twig Logic

Next consider this snippet of twig markup found in the artistCamp.page.twig file:

{% set containerized_routes = ['user.login', 'user.register', 'node.add','node.add_page','entity.user.edit_form','comment.admin','view.files.page_1','view.message.page_1']  %}
<div id="page-instance-fluid" class="page-instance-fluid">
<main id="main">
{% if current_route in containerized_routes %}
<div class="container">
{{ page.content }}
</div>
{% else %}
{{ page.content }}
{% endif %}

It’s easy to see we have a small piece of logic that checks our custom defined and available ‘current_route’ variable, and depending on its value either outputs the {{ page.content }} region in a container or not.

Finally it’s worth mentioning that you can include twig templates inside of other twig templates using the following syntax as we see in this file including the footer twig template:

{% include directory ~ '/templates/includes/footer/footer.html.twig' %}

*.libraries.yml

This file contains library definitions which can reference internal and external resources such as CSS and Javascript, and houses those defined resources under a custom name/ID that you choose. Let’s take a look at the artistCamp.libraries.yml file:

#Global for all pages
global-header:
  version: 1.0
  header: true
  css:
    templates/includes/css/global/temp.css: {}
    base:
templates/includes/css/global/base.min.css: { minified: true }
    layout:
templates/includes/css/global/layout.css: { minified: true }
templates/includes/css/global/layout.min.css: { minified: true }
templates/includes/css/bootstrap/bootstrap-grid.min.css: { minified: true }
    component:
templates/includes/css/custom-dropdown-menu.css: {}
https://cdnjs.cloudflare.com/ajax/libs/flickity/2.1.2/flickity.min.css: { minified: true }
https://unpkg.com/ionicons@4.2.2/dist/css/ionicons.min.css: { minified: true }
https://cdnjs.cloudflare.com/ajax/libs/plyr/3.4.6/plyr.css: {}
templates/includes/css/global/component.min.css: { minified: true }
templates/includes/css/global/header.min.css: { minified: true }
templates/includes/css/global/footer.min.css: { minified: true }
https://use.typekit.net/hkv5mea.css: {}
https://fonts/googleapis.com/css?family=Encode+Sans+Condensed: {}
    #theme:
 #templates/includes/css/global/print.css: { media: print }
  js:
    templates/includes/js/custom-dropdown-menu.js: {}
    templates/includes/js/temp.js: { weight: -5, minified: true }
    templates/includes/js/global.min.js: { weight: -5, minified: true }
    https://cdnjs.cloudflare.com/ajax/libs/plyr/3.4.6/plyr.polyfilled.min.js: {minified: true}
    https://cdnjs.cloudflare.com/ajax/libs/flickity/2.1.2/flickity.pkgd.min.js: {minified: true}
  dependencies:
    - core/jquery
    - core/jquery.once

global-footer:
  js:
    templates/includes/js/custom-plyr-implementation.js: {}

404:
  version: 2.1
  header: true
  css:
    component:
templates/includes/css/global/404.min.css: {}

If you recall, in the html.html.twig file we had:

{{attach_library('artistCamp/global-header')}}
{{attach_library('artistCamp/global-footer')}}

These two lines applied the resources defined in the libraries file to our template, and ultimately applies them to all rendered pages.

Advanced Theming

As mentioned in this tutorial, theming in Drupal 8 is very granular. That is to say, virtually any component on your webpage not only is likely theme-able, but will have dedicated theme hook names (suggestions) associated with it so that you can implement the proper files to target that component. If you have twig debugging turned on for you site, you can go to any page in your site, open developer tools, and in the markup HTML comments will be present that show you the twig theme suggestions. To clarify, this means that if you need to theme or preprocess a particular component the HTML comment will basically tell you what file you need to create so that you can target the component in question. Read more on this topic at https://www.drupal.org/docs/8/theming/twig/debugging-twig-templates.

Conclusion

Once you have implemented these files, you can go to yoursite.com/admin/appearance and install and set your theme as active.