
Theme Flip: preview another Drupal theme without changing the site
A Drupal module that previews another installed theme for one session only. How the swap works, who is allowed to use it, and what it leaves unsolved.
Get Theme Flip on Drupal.orgThe friction
Drupal changes its front-end theme in one place: the default key in system.theme config, and that change applies to every visitor at once. Checking a redesign against real content usually means a staging copy or a pile of screenshots. I built theme_flip (Theme Flip) so one person can preview a different installed theme on the live page, for their own session, without writing to system.theme.
Why a client-side toggle can’t do it
A client-side toggle (a localStorage flag, a class on the root element) can fake a light and dark switch inside one theme. It can’t load a different theme’s templates and asset libraries. That takes Drupal’s theme negotiation, which runs on the server for every request.
Contrib has an older module in this space. Switchtheme lets users choose between themes, but its project page lists it as minimally maintained with no supported stable releases. Theme Flip is a smaller, core-only take on the per-session case.
What using it looks like
Theme Flip adds a small dock in the bottom-right corner listing the allowed themes, each with its screenshot and name. Pick one and the page re-renders under that theme over AJAX, without a full reload. The address bar and browser history stay as they were, and other visitors see no change, because the choice lives in the picker’s own session.
The swap is a ThemeNegotiator service, FlipThemeNegotiator, that reads a session value on every request. It skips admin routes through core’s AdminContext::isAdminRoute(), and it never trusts the session value on its own:
private function getValidSessionTheme(): ?string { $request = $this->requestStack->getCurrentRequest(); if (!$request || !$request->hasSession()) { return NULL; }
$theme = $request->getSession()->get(self::SESSION_KEY); if (!is_string($theme) || $theme === '') { return NULL; }
$allowed = (array) $this->configFactory->get('theme_flip.settings')->get('themes'); if (!in_array($theme, $allowed, TRUE) || !$this->themeHandler->themeExists($theme)) { return NULL; }
return $theme;}A stale session, a tampered value, or a theme an admin has since removed from the list all fall back to the site’s normal theme.
Picking a theme POSTs to /theme-flip/switch/{theme}. SwitchController::switchTheme() stores the choice in the session, resets the theme manager’s cached active theme, and renders the visitor’s current path through an HTTP kernel sub-request, the same mechanism core’s comment permalink controller uses. The client swaps the returned <head> and <body> into the live page, re-runs scripts, and calls Drupal.attachBehaviors(). The preview lasts as long as the session.
CSRF for anonymous sessions
Switching a theme is a POST, and anonymous visitors can make it. Core’s _csrf_request_header_token route requirement is enforced by CsrfRequestHeaderAccessCheck, which only validates the token for authenticated users with a session, so it does nothing for anonymous requests.
Theme Flip validates the token itself. The client fetches one from /theme-flip/token the first time someone picks a theme and sends it back as an X-CSRF-Token header, which switchTheme() checks before touching the session. The token route also writes one session value, because generating a token alone doesn’t cause Drupal to send a session cookie. Visitors who never use the dock never get that cookie from Theme Flip.
The theme list is config
The allowed themes live in theme_flip.settings:themes, editable at Configuration › User interface › Theme Flip by anyone with the administer theme flip permission. On install, theme_flip_install() seeds that list with every theme under themes/custom/ that ships a screenshot.png, skipping the configured admin theme and any hidden theme, and installs any of those themes that aren’t already installed. Contrib and core themes are left out of the seed and can be added on the settings form.
The Visibility tab on the same form defaults to hiding the dock on /canvas, /canvas/*, /layout_builder/* and /node/*/layout. That list only controls where the dock appears. A theme already chosen in the session still applies on any non-admin route, the Canvas editor included.
Uninstalling removes the module and its config. Themes it installed stay installed, since one of them may have become the default theme or be in use elsewhere.
What I haven’t solved
The preview is tied to one session. Someone who wants their choice on another device, or remembered after the session ends, needs something built on top; Theme Flip persists nothing outside the session.
It also requires Drupal 10.2 or later, because the settings form uses #config_target, which arrived in that release.
What this came from
- ModuleTheme Flip
- ModuleSwitchtheme