Scoped CSS theme provider using a CSS-first, data-attribute approach.
theme_provider/1 wraps its children in a <div> and sets the
data-phia-theme attribute on it. A static CSS file (phia-themes.css,
generated by mix phia.theme install) defines CSS custom property overrides
for each [data-phia-theme="name"] selector. Because CSS cascades, every
PhiaUI component inside the wrapper automatically uses the theme's color
tokens without any additional props.
Architecture: CSS-first theming
The v2 theme system is completely CSS-first — no <style> tags are injected
at runtime. The flow is:
mix phia.theme install
│
▼
assets/css/phia-themes.css
┌─────────────────────────────────────────────────────────────────┐
│ [data-phia-theme="blue"] { --primary: oklch(...); ... } │
│ [data-phia-theme="rose"] { --primary: oklch(...); ... } │
│ ... (one block per preset × dark/light variant) │
└─────────────────────────────────────────────────────────────────┘
│
▼ CSS cascades into children
<div data-phia-theme="blue">
<.button>Blue primary button</.button>
<!-- --primary resolves to the blue preset value -->
</div>This is more efficient than runtime <style> injection because:
- The CSS is already in the browser's CSSOM on first paint
- No JavaScript is needed to apply theme colors
- Multiple scoped themes on the same page work without conflict
- Dark mode variants are handled entirely by CSS
Setup
Generate the theme CSS file:
mix phia.theme installThis creates assets/css/phia-themes.css and adds an @import to
assets/css/app.css. You can also import it manually:
@import "./phia-themes.css";Basic usage — atom shorthand
<.theme_provider theme={:blue}>
<section class="p-4 rounded-lg">
<.button>Blue button</.button>
<.badge>Blue badge</.badge>
</section>
</.theme_provider>Usage with Theme struct
Useful when the theme is loaded from the database or application config:
# In your LiveView mount:
{:ok, theme} = PhiaUi.Theme.get(:rose)
{:ok, assign(socket, org_theme: theme)}
# In the template:
<.theme_provider theme={@org_theme}>
<%= render_slot(@inner_block) %>
</.theme_provider>Multiple themes on one page
Because theming is attribute-scoped, different sections of the same page can use different themes simultaneously — a powerful feature for multi-tenant UIs or theme preview pages:
<.metric_grid cols={3}>
<.theme_provider theme={:blue}>
<.stat_card title="Plan A" value="$12/mo" trend={:up} trend_value="+5%" />
</.theme_provider>
<.theme_provider theme={:rose}>
<.stat_card title="Plan B" value="$49/mo" trend={:up} trend_value="+12%" />
</.theme_provider>
<.theme_provider theme={:green}>
<.stat_card title="Plan C" value="$99/mo" trend={:up} trend_value="+3%" />
</.theme_provider>
</.metric_grid>Runtime theme switching
To switch the entire page theme at runtime (e.g. from a user preferences
panel), use the PhiaTheme JS hook from priv/templates/js/hooks/theme.js.
The hook reads the data-theme attribute on the clicked element and updates
data-phia-theme on <html>, then stores the choice in
localStorage['phia-color-theme'].
For a full-page theme (not scoped to a wrapper), set data-phia-theme
directly on <html> from your anti-FOUC script (already handled by the
unified script in the DarkModeToggle module docs).
Theme values
The :theme attribute accepts three types:
| Value | Resolution |
|---|---|
:zinc, :blue, etc. | Theme.get/1 is called; uses theme.name string |
%PhiaUi.Theme{} | Uses theme.name directly |
nil (default) | No data-phia-theme attribute added; inherits parent theme |
Available presets
Run mix phia.theme list to see all built-in presets:
zinc | slate | blue | rose | orange | green | violet | neutralNesting
Themes can be nested. The inner data-phia-theme wins via CSS specificity:
<.theme_provider theme={:blue}>
<.button>Blue</.button>
<.theme_provider theme={:rose}>
<.button>Rose (overrides blue)</.button>
</.theme_provider>
</.theme_provider>
Summary
Functions
Wraps content in a scoped CSS theme context.
Functions
Wraps content in a scoped CSS theme context.
Sets data-phia-theme={theme_name} on the wrapper <div>, which activates
the matching CSS custom property block from phia-themes.css. All PhiaUI
components inside the wrapper resolve their color tokens (primary, secondary,
muted, etc.) from the theme's CSS variables.
When theme is nil, the attribute is omitted and the div is rendered as
a neutral wrapper without any theme override.
Example
<.theme_provider theme={:blue} class="rounded-lg border p-4">
<.button>Blue button</.button>
<.badge variant={:default}>Blue badge</.badge>
</.theme_provider>Attributes
theme(:any) - The theme to activate inside the wrapper. Accepts:- An atom matching a built-in preset (
:zinc,:blue,:slate,:rose,:orange,:green,:violet,:neutral) — resolved viaTheme.get/1 - A
%PhiaUi.Theme{}struct — usestheme.namedirectly nil(default) — nodata-phia-themeattribute is set; the element inherits whatever theme is set on an ancestor (or the page default)
Defaults to
nil.- An atom matching a built-in preset (
class(:string) - Additional CSS classes applied to the wrapper<div>. The wrapper is a plain<div>with no visual styling of its own — use:classto add layout, padding, or background classes if needed.Defaults to
nil.Global attributes are accepted. HTML attributes forwarded to the wrapper div (e.g.
id,style,data-*).
Slots
inner_block(required) - Content rendered inside the themed scope. All PhiaUI components inside will use the specified theme's CSS custom properties.