Choose your theme dynamically in Drupal 8 with theme negotiation
Have you ever needed to render certain pages (or groups of pages) with a different theme than the default one configured for the site? I did. And in this article I'm going to show you how it's done in Drupal 8. And like usual, I will illustrate the technique using a simple use case.
The requirement
Let's say we have a second theme on our site called gianduja
since we just love the chocolate from Torino so much. And we want to apply this theme to a few custom routes (the content rendered by the respective controllers is not so important for this article). How would we go about implementing this in a custom module called Gianduja
?
The solution
First, we need a route option to distinguish these routes as needing a different theme. We can call this option _custom_theme
and its value can be the machine name of the theme we want to render with it. This is how a route using this option would look like:
gianduja.info:
path: '/gianduja/info'
defaults:
_controller: '\Drupal\gianduja\Controller\GiandujaController::info'
_title: 'About Gianduja'
requirements:
_permission: 'access content'
options:
_custom_theme: 'gianduja'
Just a simple route for our first info page. You can see our custom option at the bottom which indicates the theme this route should use to render its content in. The Controller implementation is outside the scope of this article.
However, just adding an option there won't actually do anything. We need to implement a ThemeNegoatiator
that looks at the routes as they are requested and switches the theme if needed. We do this by creating a tagged service.
So let's create a simple class for this service inside the src/Theme
folder (directory/namespace not so important):
namespace Drupal\gianduja\Theme;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Theme\ThemeNegotiatorInterface;
/**
* Our Gianduja Theme Negotiator
*/
class ThemeNegotiator implements ThemeNegotiatorInterface {
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
$route = $route_match->getRouteObject();
if (!$route instanceof Route) {
return FALSE;
}
$option = $route->getOption('_custom_theme');
if (!$option) {
return FALSE;
}
return $option == 'gianduja';
}
/**
* {@inheritdoc}
*/
public function determineActiveTheme(RouteMatchInterface $route_match) {
return 'gianduja';
}
}
As you can see, all we need to do is implement the ThemeNegotiatorInterface
which comes with two methods. The first, applies()
, is the most important. It is run on each route to determine if this negotiator provides the theme for it. So in our example we examine the Route object and see if it has the option we set in our route. The second, determineActiveTheme()
is responsible for providing the theme name to be used in case applies()
has returned TRUE for this route. So here we just return our theme name. All pretty straightforward.
Lastly though, we need to register this class as a service in our gianduja.services.yml
file:
services:
theme.negotiator.gianduja:
class: Drupal\gianduja\Theme\ThemeNegotiator
tags:
- { name: theme_negotiator, priority: -50 }
This is a normal definition of a service, except for the fact that we are applying the theme_negotiator
tag to it to inform the relevant container compiler pass that we are talking about a theme negotiator instance. Additionally, we are also setting a priority for it so that it runs early on in the theme negotiation process.
And that is pretty much it. Clearing the cache and hitting our new route should use the gianduja
theme if one exists and is enabled.
Using this example, we can create more complex scenarios as well. For one, the theme negotiator class can receive services from the container if we just name them in the service definition. Using these we can then run complex logics to determine whether and which theme should be used on a certain route. For example, we can look at a canonical route of an entity and render it with a different theme if it has a certain taxonomy tag applied to it. There is quite a lot of flexibility here.