Styling Components
View SourceThis guide covers how to define and compose styles for your Phoenix components.
Basic Usage
Use class/2 to define named styles:
defmodule MyAppWeb.Button do
use Phoenix.Component
use LiveStyle
class :base,
display: "inline-flex",
align_items: "center",
justify_content: "center",
padding: "8px 16px",
border_radius: "6px",
font_weight: "500",
cursor: "pointer"
class :primary,
background_color: "#4f46e5",
color: "white"
def button(assigns) do
~H"""
<button {css([:base, :primary])}>
<%= render_slot(@inner_block) %>
</button>
"""
end
endUsing Tokens
Reference tokens using var for colors/themed values and const for static values:
defmodule MyAppWeb.Card do
use LiveStyle
class :card,
# Static values use const
padding: const({MyAppWeb.Spacing, :md}),
border_radius: const({MyAppWeb.Radius, :lg}),
font_size: const({MyAppWeb.FontSize, :base}),
box_shadow: const({MyAppWeb.Shadow, :md}),
# Colors use var (for theming)
background_color: var({MyAppWeb.Semantic, :fill_surface}),
color: var({MyAppWeb.Semantic, :text_primary})
endPseudo-classes and States
Group conditions for a property using a list of key-value pairs:
class :link,
color: [
default: var({MyAppWeb.Semantic, :text_link}),
":hover": var({MyAppWeb.Colors, :indigo_700}),
":focus": var({MyAppWeb.Colors, :indigo_800})
],
text_decoration: [
default: "none",
":hover": "underline"
]
class :input,
border_color: [
default: var({MyAppWeb.Semantic, :border_default}),
":focus": var({MyAppWeb.Semantic, :border_focus}),
":disabled": var({MyAppWeb.Colors, :gray_200})
]Media Queries
Responsive styles follow the same pattern:
class :container,
padding: [
default: const({MyAppWeb.Spacing, :md}),
"@media (min-width: 768px)": const({MyAppWeb.Spacing, :lg}),
"@media (min-width: 1024px)": const({MyAppWeb.Spacing, :xl})
],
max_width: [
default: "100%",
"@media (min-width: 1280px)": "1280px"
]Using breakpoint constants with string interpolation:
class :grid,
display: "grid",
grid_template_columns: [
default: "1fr",
"@media #{const({MyAppWeb.Breakpoints, :md})}": "repeat(2, 1fr)",
"@media #{const({MyAppWeb.Breakpoints, :lg})}": "repeat(3, 1fr)"
]Pseudo-elements
class :required_field,
position: "relative",
"::before": [
content: "'*'",
color: var({MyAppWeb.Colors, :red_500}),
position: "absolute",
left: "-1em"
]
class :custom_checkbox,
"::after": [
content: "''",
display: "block",
width: "16px",
height: "16px",
background_color: [
default: "transparent",
":checked": var({MyAppWeb.Semantic, :fill_primary})
]
]Style Composition
Include from Other Modules
defmodule MyAppWeb.BaseStyles do
use LiveStyle
class :button_base,
display: "inline-flex",
padding: const({MyAppWeb.Spacing, :md}),
border: "none",
cursor: "pointer"
end
defmodule MyAppWeb.Button do
use LiveStyle
class :primary, [
include({MyAppWeb.BaseStyles, :button_base}),
background_color: var({MyAppWeb.Semantic, :fill_primary}),
color: var({MyAppWeb.Semantic, :text_inverse})
]
class :secondary, [
include({MyAppWeb.BaseStyles, :button_base}),
background_color: var({MyAppWeb.Semantic, :fill_secondary}),
color: var({MyAppWeb.Semantic, :text_primary})
]
endSelf-Reference (Same Module)
defmodule MyAppWeb.Card do
use LiveStyle
class :base,
border_radius: const({MyAppWeb.Radius, :lg}),
padding: const({MyAppWeb.Spacing, :md}),
background_color: var({MyAppWeb.Semantic, :fill_card})
class :elevated, [
include(:base),
box_shadow: const({MyAppWeb.Shadow, :md})
]
class :outlined, [
include(:base),
border_width: "1px",
border_style: "solid",
border_color: var({MyAppWeb.Semantic, :border_default})
]
endConditional Styles
Use Elixir's boolean logic for conditional class application:
def button(assigns) do
~H"""
<button {css([
:base,
@variant == :primary && :primary,
@variant == :secondary && :secondary,
@disabled && :disabled,
@size == :small && :small
])}>
<%= render_slot(@inner_block) %>
</button>
"""
endDynamic Styles
For styles that depend on runtime values, use a function:
defmodule MyAppWeb.Components do
use LiveStyle
class :dynamic_opacity, fn opacity ->
[opacity: opacity]
end
class :dynamic_color, fn r, g, b ->
[color: "rgb(#{r}, #{g}, #{b})"]
end
class :dynamic_size, fn width, height ->
[width: "#{width}px", height: "#{height}px"]
end
endUse dynamic styles with css/1:
<div {css([{:dynamic_opacity, 0.5}])}>
Faded content
</div>
<div {css([{:dynamic_color, [255, 0, 0]}])}>
Red text
</div>Merging Static and Dynamic Styles
<div {css([
:card,
:elevated,
{:dynamic_opacity, @opacity},
@is_active && :active
])}>
Card content
</div>The list can contain:
- Atoms (static style names)
{atom, args}tuples (dynamic styles with arguments)nilorfalse(ignored, useful for conditionals)
Fallback Values
Use fallback/1 for CSS fallbacks:
class :sticky_header,
position: fallback(["sticky", "-webkit-sticky", "fixed"])
class :modern_layout,
display: fallback(["grid", "flex"])Cross-Module Style Access
Access styles from other modules in templates using tuple syntax:
<button {css({MyAppWeb.Button, :primary})}>
Click me
</button>Next Steps
- Theming - Create theme variations
- Advanced Features - Contextual selectors and view transitions