Theming
View SourceIntroduction
This guide explains how to set up theme selection using the data-theme attribute and the Corex.Select component in Phoenix LiveView.
Theme (neo, uno, duo, leo) is independent from mode (light/dark). The CSS token system uses both: [data-theme="neo"][data-mode="dark"]. See Dark Mode for mode setup.
This approach uses Plugs to load the correct theme on initial render, avoiding FOUC (Flash of Unstyled Content).
Implementation
1. Create the Theme Plug
Create a plug that reads the theme from the phx_theme cookie and puts it in assigns and session:
defmodule MyAppWeb.Plugs.Theme do
@moduledoc """
Reads the theme from the phx_theme cookie and puts it in assigns and session.
Allows the server to render the correct theme in the initial HTML (no flash).
"""
import Plug.Conn
@valid_themes ~w(neo uno duo leo)
def init(opts), do: opts
def call(conn, _opts) do
theme =
conn.cookies["phx_theme"]
|> parse_theme()
conn
|> assign(:theme, theme)
|> put_session(:theme, theme)
end
defp parse_theme(nil), do: "neo"
defp parse_theme(theme) when theme in @valid_themes, do: theme
defp parse_theme(_), do: "neo"
end2. Add the Plug to Your Router
Add the Theme plug to your browser pipeline:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug MyAppWeb.Plugs.Theme
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end3. Create the Theme Live Hook
Create a LiveView hook that assigns the theme from the session:
defmodule MyAppWeb.ThemeLive do
@moduledoc """
Assigns the theme from the session to the LiveView socket.
"""
def on_mount(:default, _params, session, socket) do
theme = session["theme"] || "neo"
{:cont, Phoenix.Component.assign(socket, :theme, theme)}
end
endAdd it to your live sessions:
live_session :default, on_mount: [MyAppWeb.ThemeLive, MyAppWeb.SharedEvents] do
live "/", PageLive, :index
end4. Configure Your Root Layout
Update your root.html.heex to use a dynamic data-theme attribute and add the theme script:
<html lang="en" data-theme={assigns[:theme] || "neo"}>
<head>
<script>
(() => {
const validThemes = ["neo", "uno", "duo", "leo"];
const setTheme = (theme) => {
const resolved = validThemes.includes(theme) ? theme : "neo";
localStorage.setItem("phx:theme", resolved);
document.cookie = "phx_theme=" + resolved + "; path=/; max-age=31536000";
document.documentElement.setAttribute("data-theme", resolved);
};
setTheme(localStorage.getItem("phx:theme") || document.documentElement.getAttribute("data-theme") || "neo");
window.addEventListener("storage", (e) => e.key === "phx:theme" && e.newValue && setTheme(e.newValue));
window.addEventListener("phx:set-theme", (e) => {
const value = e.detail?.value;
const theme = Array.isArray(value) && value[0] ? value[0] : "neo";
setTheme(theme);
});
})();
</script>
</head>
<body>
{@inner_content}
</body>
</html>5. Theme Select Component
Add a theme switcher using the Select component:
attr :theme, :string,
default: "neo",
values: ["neo", "uno", "duo", "leo"],
doc: "the theme from cookie/session"
def theme_toggle(assigns) do
~H"""
<.select
id="theme-select"
class="select select--sm select--micro"
collection={[
%{id: "neo", label: "Neo"},
%{id: "uno", label: "Uno"},
%{id: "duo", label: "Duo"},
%{id: "leo", label: "Leo"}
]}
value={[@theme]}
on_value_change_client="phx:set-theme"
>
<:label class="sr-only">
Theme
</:label>
<:item :let={item}>
{item.label}
</:item>
<:trigger>
<.heroicon name="hero-swatch" />
</:trigger>
<:item_indicator>
<.heroicon name="hero-check" />
</:item_indicator>
</.select>
"""
end6. Styling
Import all theme CSS files so switching works. Each theme has light and dark variants, combined with data-mode:
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/tokens/themes/neo/dark.css";
@import "../corex/tokens/themes/uno/light.css";
@import "../corex/tokens/themes/uno/dark.css";
@import "../corex/tokens/themes/duo/light.css";
@import "../corex/tokens/themes/duo/dark.css";
@import "../corex/tokens/themes/leo/light.css";
@import "../corex/tokens/themes/leo/dark.css";Available Themes
- neo
- uno
- duo
- leo
Theme and mode are independent. The active selector is [data-theme="neo"][data-mode="dark"] for Neo in dark mode.
Summary
- The theme is read from the
phx_themecookie on initial load (via Theme plug) - The theme is stored in assigns and session
- A client-side script in
root.html.heexinitializes and persists the theme - LiveView receives the theme via the ThemeLive
on_mounthook - The Select component dispatches
phx:set-themeon change - Both cookie and localStorage stay in sync for server and client
Theme logic is separate from mode logic: use distinct plugs, hooks, cookies, and scripts for each.