How can I programmatically change the active Drupal 8 theme?

In Drupal 6, we used the following code.

global $custom_theme;
$custom_theme = 'garland';

In Drupal 7, we used hook_custom_theme().

In Drupal 8, what's the correct way to do this?

3 Answers 3


In Drupal 8, you use theme negotiators, which essentially are services using a specific tag. See the theme negotiators implemented by Drupal, to understand exactly how they work; the example given in the change record is not updated.


    class: Drupal\user\Theme\AdminNegotiator
    arguments: ['@current_user', '@config.factory', '@entity_type.manager', '@router.admin_context']
      - { name: theme_negotiator, priority: -40 }



namespace Drupal\user\Theme;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\AdminContext;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Theme\ThemeNegotiatorInterface;

 * Sets the active theme on admin pages.
class AdminNegotiator implements ThemeNegotiatorInterface {
  use DeprecatedServicePropertyTrait;

   * {@inheritdoc}
  protected $deprecatedProperties = ['entityManager' => 'entity.manager'];

   * The current user.
   * @var \Drupal\Core\Session\AccountInterface
  protected $user;

   * The config factory.
   * @var \Drupal\Core\Config\ConfigFactoryInterface
  protected $configFactory;

   * The entity type manager.
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  protected $entityTypeManager;

   * The route admin context to determine whether a route is an admin one.
   * @var \Drupal\Core\Routing\AdminContext
  protected $adminContext;

   * Creates a new AdminNegotiator instance.
   * @param \Drupal\Core\Session\AccountInterface $user
   *   The current user.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Routing\AdminContext $admin_context
   *   The route admin context to determine whether the route is an admin one.
  public function __construct(AccountInterface $user, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, AdminContext $admin_context) {
    $this->user = $user;
    $this->configFactory = $config_factory;
    $this->entityTypeManager = $entity_type_manager;
    $this->adminContext = $admin_context;

   * {@inheritdoc}
  public function applies(RouteMatchInterface $route_match) {
    return ($this->entityTypeManager->hasHandler('user_role', 'storage') && $this->user->hasPermission('view the administration theme') && $this->adminContext->isAdminRoute($route_match->getRouteObject()));

   * {@inheritdoc}
  public function determineActiveTheme(RouteMatchInterface $route_match) {
    return $this->configFactory->get('system.theme')->get('admin');


The code is pretty easy to understand: The applies() method returns TRUE when the current route is the one for which your module wants to change the theme; the determineActiveTheme() method returns the theme machine name of the theme to apply.

See also ThemeNegotiator::determineActiveTheme() should not require a RouteMatch to be passed in for a possible change on the arguments received from the methods used by theme negotiators; if that patch is applied, you will need to change your theme negotiator code too.
That issue's status has been Needs work since May 10, 2018; it has been also moved to Drupal 9.2.x on December 28, 2018. Drupal 8.9.x could not get that change back-ported and it's probably safe to say that a Drupal 8.x module needs to use the RouteMatchInterface $route_match parameter for applies().

  • Shouldn't applies() be written as applies($route_match) in the above example? Posted the same question to the linked d.o. page. Thanks! Commented May 22, 2016 at 15:41
  • @StefanosPetrakis Hmmm... Any current implementation does get that as parameter, contrary to what that change record says.
    – apaderno
    Commented May 22, 2016 at 17:04
  • I have updated the answer, using the code Drupal core is actually using in one of its theme negotiators.
    – apaderno
    Commented May 22, 2016 at 17:17

We have also a very nice and clean solution at https://git.drupalcode.org/project/mailsystem/blob/HEAD/src/MailsystemManager.php#n60

 // Switch the theme to the configured mail theme.
$mail_theme = $this->getMailTheme();
$current_active_theme = $this->themeManager->getActiveTheme();
if ($mail_theme && $mail_theme != $current_active_theme->getName()) {

Another solution to this question is using the Theme Switcher module. it's compatible with Drupal 8 and 9 that developed by themeKey module developer.

Theme Switcher module allows you to create theme-switching rules which allow automatic selection of a theme based on Drupal 8 Conditions system. Because of this it can also be easily extended to support additional custom conditions exposed by other modules. In Drupal 8 conditions are no longer defined through a hook and they are plugins now.

With this module you can apply a specific theme to a unique node, use different themes based on the page language, use contrib conditions like a domain to apply one theme or another. The best of all is that you can do al of the above at once!

  • Yep it's a good module, but behaves unexpectedly when multiple rules are applied
    – camslice
    Commented Sep 13, 2021 at 18:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.