Theming
View SourceLiveStyle provides a powerful theming system that allows you to override CSS variables for different contexts, such as dark mode or high contrast themes.
The Theming Pattern
The standard pattern uses two layers:
- Colors - Raw color palette using
vars(hex values) - Semantic tokens - Abstract meanings that reference colors via
var(themed)
This separation keeps color values in one place while allowing themes to swap which colors semantic tokens point to.
Note: Only values defined with
varscan be themed. Many teams keep spacing/typography/radii asconsts, but if you want themeable spacing scales (e.g. compact/cozy), define them withvarsand override withtheme_class.
Defining Themes
Using the module-as-namespace pattern, define colors in one module and semantic tokens in another:
defmodule MyAppWeb.Colors do
use LiveStyle
vars [
white: "#ffffff",
black: "#000000",
gray_50: "#f9fafb",
gray_100: "#f3f4f6",
gray_800: "#1f2937",
gray_900: "#111827",
indigo_500: "#6366f1",
indigo_600: "#4f46e5"
]
end
defmodule MyAppWeb.Semantic do
use LiveStyle
vars [
text_primary: var({MyAppWeb.Colors, :gray_900}),
text_secondary: var({MyAppWeb.Colors, :gray_800}),
text_inverse: var({MyAppWeb.Colors, :white}),
fill_page: var({MyAppWeb.Colors, :white}),
fill_surface: var({MyAppWeb.Colors, :gray_50}),
fill_primary: var({MyAppWeb.Colors, :indigo_600}),
fill_primary_hover: var({MyAppWeb.Colors, :indigo_500})
]
# Dark theme overrides
theme_class :dark, [
text_primary: var({MyAppWeb.Colors, :gray_50}),
text_secondary: var({MyAppWeb.Colors, :gray_100}),
text_inverse: var({MyAppWeb.Colors, :gray_900}),
fill_page: var({MyAppWeb.Colors, :gray_900}),
fill_surface: var({MyAppWeb.Colors, :gray_800}),
fill_primary: var({MyAppWeb.Colors, :indigo_500}),
fill_primary_hover: var({MyAppWeb.Colors, :indigo_600})
]
end
defmodule MyAppWeb.Spacing do
use LiveStyle
consts [
sm: "8px",
md: "16px",
lg: "24px"
]
end
defmodule MyAppWeb.Radius do
use LiveStyle
consts [
md: "8px",
lg: "12px"
]
endUsing Semantic Tokens
Components reference semantic tokens for colors, constants for static values:
defmodule MyAppWeb.Card do
use LiveStyle
class :card,
# Colors use var (themed)
background_color: var({MyAppWeb.Semantic, :fill_surface}),
color: var({MyAppWeb.Semantic, :text_primary}),
# Static values use const (not themed)
padding: const({MyAppWeb.Spacing, :md}),
border_radius: const({MyAppWeb.Radius, :lg})
end
defmodule MyAppWeb.Button do
use LiveStyle
class :primary,
background_color: var({MyAppWeb.Semantic, :fill_primary}),
color: var({MyAppWeb.Semantic, :text_inverse}),
padding: const({MyAppWeb.Spacing, :md}),
border_radius: const({MyAppWeb.Radius, :md}),
":hover": [
background_color: var({MyAppWeb.Semantic, :fill_primary_hover})
]
endComponents don't need to know about themes - they just use semantic tokens for colors.
Applying Themes
Use theme_class/1 to apply a theme to a subtree:
<div class={theme_class({MyAppWeb.Semantic, :dark})}>
<!-- All children use dark theme colors -->
<.card>
<p>This card uses dark theme colors</p>
<.button>Dark button</.button>
</.card>
</div>App-Wide Theme Toggle
Apply the theme at the root level:
<!-- In root.html.heex -->
<html class={@theme == :dark && theme_class({MyAppWeb.Semantic, :dark})}>
<head>
<!-- ... -->
</head>
<body {css({MyAppWeb.Layout, :body})}>
<%= @inner_content %>
</body>
</html>Respecting System Preference
For automatic system preference, use JavaScript to detect and set a class on the <html> element:
// Check system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark-theme');
}Then apply the theme conditionally in your template.
Multiple Themes
Define multiple themes for different contexts:
defmodule MyAppWeb.Colors do
use LiveStyle
vars [
white: "#ffffff",
black: "#000000",
gray_900: "#111827",
yellow_400: "#facc15"
]
end
defmodule MyAppWeb.Semantic do
use LiveStyle
vars [
text_primary: var({MyAppWeb.Colors, :gray_900}),
fill_page: var({MyAppWeb.Colors, :white})
]
# Dark theme
theme_class :dark, [
text_primary: var({MyAppWeb.Colors, :white}),
fill_page: var({MyAppWeb.Colors, :gray_900})
]
# High contrast theme
theme_class :high_contrast, [
text_primary: var({MyAppWeb.Colors, :black}),
fill_page: var({MyAppWeb.Colors, :white})
]
# Special promotion theme
theme_class :promo, [
text_primary: var({MyAppWeb.Colors, :black}),
fill_page: var({MyAppWeb.Colors, :yellow_400})
]
endUse different themes in different parts of your app:
<main>
<!-- Default theme -->
<.hero />
<!-- Promotional section -->
<section class={theme_class({MyAppWeb.Semantic, :promo})}>
<.promo_banner />
</section>
<!-- Footer with dark theme -->
<footer class={theme_class({MyAppWeb.Semantic, :dark})}>
<.footer_content />
</footer>
</main>Nested Themes
Themes can be nested - inner themes override outer ones:
<div class={theme_class({MyAppWeb.Semantic, :dark})}>
<!-- Dark theme -->
<.card>Dark card</.card>
<div class={theme_class({MyAppWeb.Semantic, :high_contrast})}>
<!-- High contrast theme (overrides dark) -->
<.card>High contrast card</.card>
</div>
</div>Theme-Aware Components
Sometimes you need different behavior based on the active theme. While components shouldn't need to know about themes for basic styling (that's what semantic tokens are for), you might need theme awareness for:
- Icons that need different assets
- Complex components with non-CSS differences
For these cases, pass the theme as a prop:
def logo(assigns) do
~H"""
<img src={logo_src(@theme)} alt="Logo" />
"""
end
defp logo_src(:dark), do: ~p"/images/logo-light.svg"
defp logo_src(_), do: ~p"/images/logo-dark.svg"Best Practices
- Use semantic tokens for colors - Don't reference color primitives directly in components
- Use constants for static values - Spacing, typography, radii don't need theming
- Keep primitives stable - Themes override semantics, not primitives
- Name semantically - Use
fill_primarynotfill_blue - Test all themes - Ensure components look good in every theme
- Consider accessibility - Ensure sufficient contrast in all themes
Next Steps
- Advanced Features - Contextual selectors and view transitions
- Configuration - CSS layers and other options