Profile picture for user admin
Daniel Sipos
09 Jan 2017

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 ThemeNegotiator 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 Symfony\Component\Routing\Route;
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.

Profile picture for user admin

Daniel Sipos

CEO @ Web Omelette

Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.

Contact us

Comments

Fons Vandamme 10 Jan 2017 13:20

Nice solution

Nice solution, if you want a more "dynamic" approach I recommend creating a context reaction to change the theme dynamically based on content conditions.

Anonymous 10 May 2017 04:40

Awesome! works like a charm!

Awesome! works like a charm! thanks for writing/sharing your posts!

Calvin Choe 01 Aug 2018 19:58

Need to import properly namespaced Route class

I implemented this quickly but it wasn't working for me. I realized that there is a check to see if the $route is an instance of class Route. However, you need to properly namespace Route so that this check can pass, otherwise it will never switch the theme.

So at the top of "ThemeNegotiator.php" simply add:

use Symfony\Component\Routing\Route;

Then it will use the properly namespaced Route and not Drupal\gianduja\Theme\Route (as it is trying to do in this example).

Odai Atieh 12 Sep 2018 13:57

Name tag in single quotation

Great solution, thank you Danny.
I think the name tag in services file should be in a single quotation, so it will be:

services:
    theme.negotiator.gianduja:
        class: Drupal\gianduja\Theme\ThemeNegotiator
        tags:
          - { name: 'theme_negotiator', priority: -50 }
Matthieu 23 May 2022 16:45

Higher priority required

For those who try this method but do not manage to get their theme enable, please try to use a different priority in the service declaration, as follow:


services:
theme.negotiator.gianduja:
class: Drupal\gianduja\Theme\ThemeNegotiator
tags:
- { name: 'theme_negotiator', priority: 100 }

Add new comment