How to theme
Drupal 8
Setup
Installation
Preparation
Create a theme
File structure
- Open your Drupal project with a good editor
- PHPStorm
- Visual Studio
- Brackets?
- Go to the map /themes
- All themes are installed in this folder
- Create a new folder
- Choose a sensible name

Picking a name
Each individual theme is contained in a directory named after the theme itself. For example fluffiness/.
The name must be all lowercase, start with a letter, and uses an underscore (_) instead of spaces.
This name will be important, a lot of files will have a reference to this name.
Changing the name later can be annoying.
Info File
Create a theme
Look at the examples
Your Drupal installation already contains a number of themes.
As example check these out:
- /core/themes/bartik
- /core/themes/classy
- /core/profiles/demo_umami/themes/umami
Create .info.yml
In our new folder (F.E.: /themes/fluffiness) add a new file. Name this file:
<themename.info.yml>
F.E.:
- fluffiness.info.yml
- d8theming.info.yml
This name must be the same as the folder name!
Add to info .info.yml
Add some variables to your own info file:
name: Calypso
type: theme
base theme: classy
description: 'A theme in dedication to the goddess of the sea.'
core_version_requirement: ^8 || ^9
Activate the new theme
Go to your site, to the appearance page:
- localhost/drupal/admin/appearance
Find your newly created theme and hit Install & set as default

Success?
If all went well your theme should be active, and your site will look like this:

Docs
Adding libraries
Create a theme
Adding CSS
In your theme, add a new folder: css
/themes/fluffiness/css
In this folder, create a new file: styles.css
/themes/fluffiness/css/styles.css
Add .libraries.yml
In your theme, add a new file: <themename>.libraries.yml
/themes/fluffiness/fluffiness.libraries.yml
Look at the classy.libraries.yml file
Copy the first 4 lines to your library file:
global:
version: VERSION
css:
component:
css/styles.css: {}
Rename base.css
to styles.css
Link .libraries.yml
In your theme's info.yml file, add the following code:
libraries:
- fluffiness/global
Add some CSS
Add some CSS to your css/styles.css file for testing
F.E.:
Clear caches, refresh home & check result.
body {
background-color: hotpink;
}
Notes

Docs
Adding Assets
Create a theme
Adding Assets
Want to use assets for you CSS?
Add a new folder: images
/themes/fluffiness/assets
Add theme-assets to this folder:
- Images: assets/images
- Video: assets/video
- Fonts: assets/fonts
- etc
Adding Assets
You can these use these assets in your theme CSS:
background: url('/themes/fluffiness/assets/images/cat.jpg')
Adding Logo
Create a theme
Adding a logo.svg
You can add your own .svg logo to the theme, just place it like this:
This logo will automatically be used

Adding a logo.png
Want a non-svg logo? Add it through:
/admin/appearance/settings/d8theming

Other ways
Adding Screenshot
Create a theme
Adding a screenshot
Add a screenshot.png/.gif file to the theme:
This logo will automatically be used

Twig
Templates
Behind the hood

Example
{# region.html.twig
/**
* @file
* Theme override to display a region.
*
* Available variables:
* - content: The content for this region, typically blocks.
* - attributes: HTML attributes for the region <div>.
* - region: The name of the region variable as defined in the theme's
* .info.yml file.
*
* @see template_preprocess_region()
*/
#}
{%
set classes = [
'region',
'region-' ~ region|clean_class,
]
%}
{% if content %}
<div{{ attributes.addClass(classes) }}>
{{ content }}
</div>
{% endif %}
Twig code
{# comment #}
{{ variable }}
{% functional %}
Templates

Copy templates


Exercise
- Copy over the page template from the base theme
- Add a 'container' class
.container {
max-width: 1200px;
margin: 0 auto;
}
Exercise
- Add a new region to your theme's info file
- Look at bartik.info.yml
- Add this new region to your page template
- Add a block to your new region through structure -> blocks
Docs: https://www.drupal.org/docs/theming-drupal/adding-regions-to-a-theme
Note: if this exercise is too hard, move on to the next ones
Using templates
AKA Debugging twig
https://www.drupal.org/node/2358785
https://www.drupal.org/docs/8/theming/twig/debugging-twig-templates
services.yml

copy default.services.yml
Set debug: true
twig.config:
# Twig debugging:
#
# When debugging is enabled:
# - The markup of each Twig template is surrounded by HTML comments that
# contain theming information, such as template file name suggestions.
# - Note that this debugging markup will cause automated tests that directly
# check rendered HTML to fail. When running automated tests, 'debug'
# should be set to FALSE.
# - The dump() function can be used in Twig templates to output information
# about template variables.
# - Twig templates are automatically recompiled whenever the source code
# changes (see auto_reload below).
#
# For more information about debugging Twig templates, see
# https://www.drupal.org/node/1906392.
#
# Not recommended in production environments
# @default false
debug: false
Clear cache & Refresh!

Naming templates
Naming conventions
field.html.twig
- field--node--field-intro--article.html.twig
- field--node--field-intro.html.twig
- field--node--article.html.twig
- field--field-intro.html.twig
- field--text-long.html.twig
Naming templates
html.html.twig
- html--internalviewpath.html.twig
- html--node--id.html.twig
- html.html.twig
page.html.twig
- page--node--edit.html.twig
- page--node--1.html.twig
- page--node.html.twig
- page.html.twig
Which templates
- templates/a/node.html.twig
- templates/z/node.html.Twig
- templates/a/node.html.twig
- templates/a/node-foo.html.Twig
Filters
{% filter upper %}
uppercase is awesome
{% endfilter %}
{{ variable|upper }}
Hardcoded text
Drupal variable
More filters
{{ var|clean_class }}
{{ var|clean_id }}
{{ var|format_date }}
{{ var|raw }}
{{ var|render }}
{{ var|safe_join }}
{{ var|without }}
Drupal only filters
<a
href="{{ url('<front>') }}"
title="{{ 'Home'|t }}"
rel="home"
class="site-logo"
></a>
Translation
{% trans %}Translatable text{% endtrans %}
Without filter
{{ content|without('field') }}
Without filter example
{{ content }}
{{ text }}
{{ tags }}
{{ image }}
{{ content|without('image', 'tags') }}
{{ content|without('image', 'tags') }}
{{ text }}
{{ content.tags }}
{{ content.image }}
In code

Finding field names

Exercise
- Go to the home page
- Or create a new page
- Create a template for the paragraph 'Text with Media'
- Split the paragraph in 2 areas: left & right
- Add 'grid' classes to the markup
.row {
margin-left: -1rem;
margin-right: -1rem;
}
.column {
float: left;
padding-left: 1rem;
padding-right: 1rem;
}
@media only screen and (min-width: 768px) {
.col-md-6 {
width: 50%;
}
}
Set variables
{%
set classes = [
'node',
'node--type-' ~ node.bundle|clean_class,
node.isPromoted() ? 'node--promoted',
node.isSticky() ? 'node--sticky',
not node.isPublished() ? 'node--unpublished',
view_mode ? 'node--view-mode-' ~ view_mode|clean_class,
]
%}
Debugging vars
{{ dump(var) }} -> not reliable
{{ dpm(var) }} -> outputs a lot
{{ kpr(var) }} -> similar to dpm
{{ kint(var) }} -> most readable;
-> very heavy on memory
-> crashes a lot on big objects
Debugging vars
{{ dpm(content) }}
{# Print only field_body information #}
{{ dpm(content.field_body) }}
{# Printing image url value #}
{{ dpm(content.field_bg_image.0.entity.uri.value) }}
{# Printing a media image url value #}
{{ file_url(content.field_media_image|field_target_entity.field_media_image.entity.uri.value) }}
Exercise
- Go to the About us page
- or create a new page
- Create a template for the paragraph 'Text with background image'
- Rewrite the output of the 'Background image' field as a background image
- It's ok to use inline CSS
- Rewrite the output of the 'Text background color' as a class to style
- F.E.: 'color-primary', 'color-transparent'
- Style it
Attributes
<div class="nickelback">
<div {{
attributes
.removeClass('nickelback')
.addClass('rush')
.setAttribute('id', 'top')
}}>
<div id="top" class="rush">
For HTML attributes
Attributes
.addClass()
.removeClass()
.setAttribute()
.removeAttribute()
.hasClass()
More functions
Libraries
In Drupal 8, stylesheets (CSS) and JavaScript (JS) are loaded through the same system for modules (code) and themes, for everything: asset libraries.
Drupal uses a high-level principle: assets (CSS or JS) are still only loaded if you tell Drupal it should load them.
Drupal does not load every asset on every page, because it slows down front-end performance.
The process
- Save the CSS or JS to a file using the proper naming conventions and file structure.
- Define a "library", which can contain both CSS and JS files.
- "Attach" the library to
- all pages,
- specific Twig templates,
- a render element in a preprocess function.
The process

global-styling
global-styling:
version: 1.x
css:
theme:
css/layout.css: {}
css/style.css: {}
css/colors.css: {}
css/print.css: { media: print }
css/print.css: { media: type }
all | Suitable for all devices. |
aural | Intended for speech synthesizers. |
braille | Intended for braille tactile feedback devices. |
embossed | Intended for paged braille printers. |
handheld | Intended for handheld devices (typically small screen, monochrome, limited bandwidth). |
Intended for paged, opaque material and for documents viewed on screen in print preview mode. Please consult the section on paged media. |
projection | Intended for projected presentations, for example projectors or print to transparencies. Please consult the section on paged media. |
screen | Intended primarily for color computer screens. |
tty | Intended for media using a fixed-pitch character grid, such as teletypes, terminals, or portable devices with limited display capabilities. |
tv |
Intended for television-type devices. |
Multiple declarations
js-header:
header: true
js:
header.js: {}
js-footer:
js:
footer.js: {}
in same libraries.yml file
Weight
base:
foo.css: { weight: - 666 }
js-header:
header: true
js:
header.js: {}
Show in <head>
External
theme:
http://fonts.googleapis.com/css?
family=Loved+by+the+King: { type: external }
JavaScript
global-styling:
version: 1.x
js:
js/scripts.js: {}
JavaScript
global-styling:
version: 1.x
js:
js/scripts.js: {}
/libraries/cycle2/jquery.cycle2.min.js: {}
Place all Downloaded JS/CSS files in a folder /libraries outside of your theme!
Libraries folder

Dependencies
adding jQuery
global-styling:
version: 1.x
css:
base:
css/d8imd.css: {}
js:
js/d8imd.js: {}
dependencies:
- core/jquery
More dependencies
- core/drupal
We need core/drupal in order to take advantage of the Drupal.behaviors.
- core/jquery
To include the Drupal core version of jQuery, we add core/jquery.
- core/jquery.once
A jQuery plugin allowing to only apply a function once to an element.
More dependencies
To get a complete overview of all the core libraries, take a look inside core/core.libraries.yml.
Attaching a library
libraries:
- core/normalize
- d8imd/global-styling
d8imd.info.yml
Attaching a library
{{ attach_library('d8imd/global-styling') }}
foo.html.twig
{{ attach_library(active-theme()~'/global-styling') }}
Example
block--views-block--carousel-block-home.html.twig

Example
trainingstore_foundaton.libraries.yml

Other example
{% if id == "view_carousel" %}
{{ attach_library('theme/carousel') }}
{% endif %}
Stylesheets remove

d8imd.info.yml
Docs
Overriding and extending libraries
JavaScript
Where?

Creating .js file
Coding standards
As part of the Drupal 8 Javascript Coding Standards, all of the javascript code must be declared inside a closure wrapping the whole file. This closure must be in strict mode.
(function () {
'use strict';
// Custom javascript
})();
Plain JS
(function () {
'use strict';
window.alert("sometext");
})();

.js file: using jQuery

.js file: using jQuery
(function () {
'use strict';
$( document ).ready(function() {
console.log( "ready!" );
});
})();

But we have jQuery included?
.js file: using jQuery
(function ($) {
'use strict';
$( document ).ready(function() {
console.log( "ready!" );
});
})(jQuery);

Enter Drupal.behaviors
/**
* @file
* Placeholder file for custom sub-theme behaviors.
*
*/
(function ($, Drupal) {
'use strict';
/**
* Example drupal behavior
*/
Drupal.behaviors.exampleBehavior = {
attach: function (context, settings) {
$('.example', context).once('example-behavior').each(function () {
});
}
};
})(jQuery, Drupal);
Why Drupal.behaviors?
Drupal behaviors (Drupal.behaviors) are still part of javascript in core.
These behaviors will be executed on every request, including AJAX requests.
Calls multiple times
- After an administration overlay has been loaded into the page.
- After the AJAX Form API has submitted a form.
- When an AJAX request returns a command that modifies the HTML, such as
Can also be triggered from a module!
Behaviors examined
Including Drupal object
/**
* @file
* Placeholder file for custom sub-theme behaviors.
*
*/
(function ($, Drupal) {
})(jQuery, Drupal);
Add Drupal & jQuery.once

Behaviors examined
/**
* Example drupal behavior
*/
Drupal.behaviors.awesome = {
attach: function(context, settings) {
$('main', context).once('awesome').append('<p>Hello world</p>');
}
};
Behaviors examined
namespace:
A Drupal behavior has to have a unique namespace.
In this example, the namespace is awesome (Drupal.behaviors.awesome).
Behaviors examined
attach:
Contains the actual function that should be executed.
attach: function (context, settings) {
$('.example', context).once('example-behavior').each(function () {
alert('I\'m helping!');
});
}
Behaviors examined
once:
Using the .once('awesome') will make sure the code only runs once. Otherwise, the code will be executed on every AJAX request.
It adds a processed- class to the main tag (<main role="main" class="awesome-processed">) in order to accomplish this (core/jquery.once).
$('main', context).once('awesome').append('<p>Hello world</p>');
Advanced example
// Engine
Drupal.sbs_nu_v2.startAnimation = function() {
Drupal.sbs_nu_v2.animateActiveItem();
window.setInterval(function(){
Drupal.sbs_nu_v2.animateActiveItem();
}, 20000);
};
// Init
Drupal.behaviors.sbs_nu_v2 = {
attach: function (context, settings) {
$('#sbs-nu-v2', context).once('init-epg-box', function () {
Drupal.sbs_nu_v2.container = $(this);
Drupal.sbs_nu_v2.init();
});
}
};
Multilingual
Use Drupal.t()
// Make string available on translate interface
var close = Drupal.t('Close');
// Same, but with @placeholder
var nodesOfType = Drupal.t('Showing nodes of @type', {@type: nodeType});
Debugging behaviors?
Set breakpoint in devtools
misc/drupal.js

Debugging behaviors?
Use console.log();
console.log('Hi');
console.log(Drupal);
Docs
Exercise: mobile menu toggle
- Add a JavaScript Behavior (using jQuery) that:
- Adds a toggle button (hamburger icon) to the menu
- Make the menu open/close on click
Tips:
-
Use jQuery's '.on' & '.slidetoggle'
-
Optionally: Create a new region 'mobile menu' and duplicate the menu blocks to that menu, hide with css & media querries
.theme file
.theme

For what?
You can affect the output of certain HTML via preprocess functions.
For example, if you wanted to add a class to a menu and preferred to do this at the PHP level you can.
How?
-
Create a file in your theme directory called mytheme.theme
- Create a function such as mytheme_preprocess_HOOK where HOOK refers to the item you wish to affect.
- Write your changes and save
- Rebuild the cache so your changes are available
Start file
Add "<?php" opening-tag
Closing tag not needed
<?php
/**
* @file
* template.php
*/
// Add these when/if needed
use Drupal\Component\Utility\Html;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\media\Entity\Media;
// add functions below
Example
Add class my-menu to all menus
/**
* Implements hook_preprocess_HOOK() for menu.html.twig.
*/
function mytheme_preprocess_menu(&$variables) {
// If there is not an existing class array, create an empty array.
if (!isset($variables['attributes']['class'])) {
$variables['attributes']['class'] = [];
}
// Merge with any classes that may have been set
// by other hook_preprocess_menu invocations
$variables['attributes']['class'] =
array_merge($variables['attributes']['class'], ['my-menu']);
}
Debugging
Use var_dump(), dsm(), kpr(), kint() or XDebug
/**
* Implements hook_preprocess_HOOK() for menu.html.twig.
*/
function mytheme_preprocess_menu(&$variables) {
var_dump($variables);
var_dump($variables['main_menu']);
dsm($variables);
}
Hooks?
Use twig debug; see theme suggestions

Read the docs
https://www.drupal.org/docs/8/theming-drupal-8/modifying-attributes-in-a-theme-file
Not sure?
Consult google!
Example
Add class my-main-menu to the main menu
/**
* Implements hook_preprocess_HOOK() for menu.html.twig.
*/
function mytheme_preprocess_menu(&$variables) {
if ($variables['menu_name'] == 'main') {
if (!isset($variables['attributes']['class'])) {
$variables['attributes']['class'] = [];
}
$variables['attributes']['class'] =
array_merge($variables['attributes']['class'], ['my-main-menu']); }
}
Another example
Add page link as a var: url
/**
* Implements hook_preprocess_node().
*/
function mytheme_preprocess_node(&$variables) {
if(isset($variables['content']['#node'])) {
$node = $variables['content']['#node'];
// Add node url
$variables['url'] = $node->toUrl();
}
}
in node.html.twig template:
<a href="{{ url }}" class="inner">
Advanced example
Add some classes to our paragraphs
/**
* Implements hook_preprocess_paragraph().
*/
function mytheme_preprocess_paragraph(&$variables) {
if ($entity = $variables['elements']['#paragraph']) {
// Title align.
if ($entity->hasField('field_title_align')) {
$field_title_align = $entity->get('field_title_align')->getValue();
$field_title_align = reset($field_title_align);
if ($field_title_align) {
$variables['attributes']['class'][] = 'title-align--'
. Html::cleanCssIdentifier(reset($field_title_align));
}
}
}
}
in paragraph.html.twig template:
<section{{ attributes.addClass(classes) }}>
<div class="container">
<div class="layout paragraph__layout">
Theme settings
Where?
In Drupal 8, themes can modify the entire theme settings form by adding a PHP function to either the themename.theme file or to a themename-settings.php file.
Example
function foo_form_system_theme_settings_alter(
&$form,
\Drupal\Core\Form\FormStateInterface &$form_state,
$form_id = NULL
) {
// Work-around for a core bug affecting admin themes. See issue #943212.
if (isset($form_id)) {
return;
}
$form['foo_example'] = array(
'#type' => 'textfield',
'#title' => t('Widget'),
'#default_value' => theme_get_setting('foo_example'),
'#description' => t("Place this text in the widget spot on your site."),
);
}
Add textfield with title Widget
in themename.theme
Example
foo_example: blue bikeshed
Add config/install/THEME.settings.yml file
Set default value
$foo_example = theme_get_setting('foo_example');
Retrieve value in php
Example
<?php
function foo_preprocess_node(&$variables) {
$variables['foo_example'] = theme_get_setting('foo_example');
}
Make value available for twig files:
In themename.theme file:
{{ foo_example }}
In twig file:
Docs!
Drupal Theming IMD
By Pieter Mathys
Drupal Theming IMD
How to theme D8
- 1,079