If you develop for Drupal 9, you probably noticed that theming has improved a lot. And if you're upgrading your theme to D9, given Drupal 7's upcoming end of life in November 2022, you'll find the job considerably easier if you follow some basic but crucial steps.

Let's look at the main theming features on Drupal 9 and how to have your Drupal 7 theme ready for this latest version of Drupal.



Drupal 9's Main Theming Features

Drupal 9 has many features that make life easier for anyone who wants to upgrade their theme. Here are some of them:

  • Semantic HTML5 markup by default
  • Improved out-of-the-box accessibility features
  • Use of Twig as the theme engine instead of PHPTemplate
  • Previous theme_* functions and *.tpl.php files replaced by *.html.twig files
  • Default performance-enhancing features (like CSS and JS aggregation)
  • Responsive features OOB 
  • Attachment of JS and CSS assets by using libraries (instead of drupal_add_css and drupal_add_js)
  • CSS file structure based on SMACSS & BEM
  • CSS3 pseudo selectors
  • Classy as base theme

Of all those changes, the most important is probably the theme engine moving to Twig. Twig is a PHP-based compiled templating language, which means that when your web page is rendered, the Twig engine takes the template and converts it into a compiled PHP template stored in a protected directory in sites/default/files/php/twig. Once the compilation is done, the template files are cached for reuse and are recompiled when the Twig cache is cleared.

Your Drupal 9 Theme Migration Checklist

As for the migration of Drupal 7 themes to Drupal 9, there's no tool to do it automatically or to flag what needs changing. So when we do it, we're facing a whole theme rebuild—which can be overwhelming since we'll create a theme from scratch and replicate items manually from our D7 theme. However, having a migration checklist can help smooth the process, so these are the steps we follow for rebuilding our themes:

  1. Create the folder structure
  2. Create the necessary files
  3. Define regions
  4. Create and load your libraries
  5. Move theme settings
  6. Create breakpoints if needed 
  7. Convert templates to Twig

Let's go through each one of them!

Step 1: Create the folder structure

You should locate themes in Drupal 9 in the themes folder of the root Drupal installation. It's recommended to classify them whether they are contri or custom themes, so your structure should look something like:

 

drupal9
	/core
	/modules
	/themes
		/contrib
		/custom
			/theme_name

 

Step 2: Create the necessary files

The only file Drupal needs to recognize your theme is the .info.yml file: the old .info file is not needed anymore as it was replaced by .info.yml. This file follows yaml syntax, so remember to set the indentation to spaces instead of tabs, and respect that indentation. Here's an example, assuming we're creating a theme called simple:

name: Simple
type: theme
description: Drupal 9 version of the D7 Simple theme.
core_version_requirement: ^8 || ^9
base theme: false

The base theme key introduction is essential here. By using it, you can define the name of another theme you want to inherit from, but if you don't want your theme to be a subtheme, you still need to add the key but with a "false" value.

Step 3: Define your regions

The regions for Drupal 9 themes are defined in the theme_name.info.yml file under the key "regions":

regions:
  header: 'Header'
  navigation: 'Navigation bar'
  highlighted: 'Highlighted'
  help: 'Help'
  content: 'Content'
  sidebar_first: 'First sidebar'
  sidebar_second: 'Second sidebar'
  footer: 'Footer'

Step 4: Create and load your libraries

Instead of using the old drupal_add_js to add Javascript assets or drupal_add_css to add CSS files, you should create and load libraries. To create them, you'll need to add a new theme_name.libraries.yml file and define the libraries in it:

global_styles:
  version: 3.0.1
  css:
    theme:
      dist/css/styles.min.css: { minified: true }
  js:
    dist/js/jquery.visible.min.js: { minified: true }
    dist/js/theme.min.js: { minified: true }
  dependencies:
    - core/jquery
    - core/drupal

To load the libraries, you have many options:

  • Load libraries everywhere: add the library name in the theme_name.info.yml file under the libraries key:
                libraries:
                    - theme_name/global_styles
              
  • Load libraries conditionally in the template file: you can use the function attach_library in the template file that you want to load the library in:
    {{ attach_library('theme_name/font-awesome') }}
  • Load libraries conditionally in a pre-process function: you will use the '#attached' key in the variables received as argument:
    $variables['#attached']['library'][] = 'theme_name/lib';

You can find more information about how to override libraries and the differences between D7 and D9 here.

Step 5: Move your theme settings

Theme settings management didn't change, so you can move your theme-settings.php and it should work. Just be careful of dependencies and services you might need.

Step 6: Create breakpoints if needed

If you were using breakpoints in your theme's Drupal 7 version, you had to install the contributed module breakpoints. In Drupal 9, however, you won't need it since this functionality is now part of core. All you need is to create a new theme_name.breakpoints.yml file and add your breakpoints there. For example:

bearskin.small-width:
    label: small
    mediaQuery: '(min-width: 0px)'
    weight: 2
    multipliers:
        - 1x
bearskin.medium-width:
    label: medium
    mediaQuery: 'all and (min-width: 400px) and (max-width: 800px)'
    weight: 1
    multipliers:
        - 1x
bearskin.large-width:
    label: large
    mediaQuery: 'all and (min-width: 1000px)'
    weight: 0
    multipliers:
        - 1x
view raw

Step 7: Convert your templates

With the theme engine moving from PHPTemplate to Twig, there are some significant changes in the templates. Some online tools can help you ease this process, such as https://php2twig.com/, but in general, in the following table, you have a list of old vs. new way of writing templates:

 

PHPTemplate

Twig

Comments

<?php 

/** 

 * @file

 * File description

 */

?>

{# 

/** 

 * @file

 * File description

 */

#}

File names

html.tpl.php

page.tpl.php

node--[content-type].tpl.php

html.html.twig

page.html.twig

node--[content-type].html.twig

Function names

THEME_node_links()

node-links.html.twig

Templates and pre-process

node--event-teaser.tpl.php

hook_preprocess_node__event_teaser()

node--event-teaser.html.twig

hook_preprocess_node()

Assigning variables

<?php $tags = $content->tags; ?>

 

<?php $args = array('@author' => $author, '@date' => $created); ?>

{% set tags = content.tags %}

 

{% set args = {'@author': author, '@date': created} %}

Printing variables

<?php print $tags; ?>

 

<div class="content">

<?php print $content; ?></div>

 

<?php print $item['#item']['alt']; ?>

{{ tags }}

 

<div class="content">

{{ content }}</div>

 

{{ item['#item'].alt }}

Conditionals

<?php if ($content->tags): ?> 

<?php endif; ?>

 

<?php if ($count > 0): ?>

<?php endif; ?>

{% if content.tags %}

{% endif %}

 

{% if count > 0 %}

{% endif %}

Check if variables are empty or defined

<?php if (!empty($content->tags)): ?>

<?php endif; ?>

 

<?php if (isset($content->tags)): ?>

<?php endif; ?>

{% if content.tags is not empty %}

{% endif %}

 

{% if content.tags is defined %}

{% endif %}

Loops

<?php foreach ($users as $user) {} ?>

{% for user in users %}

{% endfor %}

Filters

<?php print check_plain($title); ?>

 

<?php print $title; ?>

{{ title|striptags }}

 

{{ title|raw }}

Translations

<?php print t('Home'); ?>

 

<?php print t('Welcome, @username', array('@username' => $user->name)); ?>

{{ 'Home'|t }}

 

{{ 'Welcome, @username'|t({ '@username': user.name }) }}

 

{% set username = user.name %}

{% trans %}

  Welcome, {{ username }}

{% endtrans %}

Imploding lists

<?php echo implode(', ', $usernames); ?>

{{ usernames | join(', ') }}

 

{{ usernames | safe_join(', ') }}

A Note About Templates and Pre-Processes

In Drupal 9, only the base hook of a pre-processing function exists. For example, in D7, you can have a "node--event-teaser.tpl.php" and subsequently a "hook_preprocess_node__event__teaser" pre-processing function available for you in your template.php file. In D8, you can still have the template file for your event teaser, but you only get the base hook, "hook_preprocess_node.", Instead, you'd need to do logic within the base to operate on your event teaser specifically.

You can find more information about the differences between PHPTemplate and Twig here.

For now, you're ready to go and start rebuilding your themes! If you need any more help, here are some extra resources: