Phoenix LiveView gives you instant interactivity—no JS build chain, no sprawling frontend stack.
But once your UI is real‑time, the visual layer matters even more: you’re shaping attention, not just serving forms.
Enter Tailwind CSS.
Why Tailwind Fits LiveView Perfectly
- Atomic utilities → express intent inline in HEEx
- No global cascade → each component styles itself
- Purge/Tree‑shake → only used classes ship
- Design tokens → consistent spacing, color, typography
LiveView renders HTML; Tailwind styles it—all server‑side, fast and consistent.
Treat Tailwind as Your Layout Language
<div class="min-h-screen flex flex-col">
<header class="bg-gray-900 text-white p-4">…</header>
<main class="flex-1 grid grid-cols-3 gap-6 p-6">
<%= for card <- @cards do %>
<.live_component module={CardComponent} card={card} />
<% end %>
</main>
</div>
-
flex
,grid
,gap-6
,p-6
—layout in plain sight - No external CSS files, no nested wrappers ― just intent
Dynamic State → Dynamic Classes
Example: Status Card
def status_bg(:idle), do: "bg-gray-100"
def status_bg(:processing), do: "bg-yellow-100"
def status_bg(:error), do: "bg-red-100"
<div class={"rounded-lg p-4 transition " <> status_bg(@status)}>
<%= @status |> to_string() |> String.capitalize() %>
</div>
- Swap classes, not DOM nodes → perceptually faster
-
transition
makes color shifts smooth
Button with Loading State
<button
phx-click="save"
class={"px-4 py-2 rounded text-white " <>
if @loading, do: "bg-gray-400 cursor-wait", else: "bg-blue-600 hover:bg-blue-700"}>
<%= if @loading, do: "Saving…", else: "Save" %>
</button>
-
Toggle
bg-gray-400
vsbg-blue-600
via a boolean assign - No JS needed; LiveView re‑renders class list instantly
Modal Without JavaScript
<%= if @show_modal do %>
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div class="bg-white p-6 rounded shadow-xl">
<h2 class="text-xl font-semibold mb-4">Modal Title</h2>
<p>Modal content…</p>
<button phx-click="close_modal" class="mt-4 bg-blue-600 text-white px-4 py-2 rounded">
Close
</button>
</div>
</div>
<% end %>
Lifecycle handled by LiveView; styling by Tailwind.
Zero JS.
Scaling Design with tailwind.config.js
module.exports = {
theme: {
extend: {
colors: { brand: '#4c51bf' },
spacing: { '128': '32rem' },
borderRadius: { 'xl': '1.25rem' },
}
}
}
Now use:
<div class="bg-brand rounded-xl p-8"></div>
Your UI stays consistent even as components update independently.
Animations & Attention
<span class={"inline-block " <> if @waiting, do: "animate-pulse", else: ""}>
<%= @text %>
</span>
-
animate-pulse
,transition-opacity
,duration-300
- Smooth feedback that feels intentional, not jarring
Why This Matters
Real‑time UIs update sections of the page asynchronously.
Visual cohesion keeps the experience seamless. Tailwind’s constrained scales + utility classes glue dynamic pieces together.
- ⚡ Fast compile (
esbuild
+ purge) - 🧹 Minimal CSS ships to the browser
- 🔄 LiveReload reflects style tweaks instantly
You get speed and coherence—no trade‑off.
Ship with Confidence
- No complex CSS cascade to debug
- No heavyweight JS framework to learn
- Just HTML + Tailwind + LiveView assigns
Write markup, style inline, let LiveView handle state.
Learn More
Grab the PDF:
Phoenix LiveView: The Pro’s Guide to Scalable Interfaces and UI Patterns
- Tailwind techniques for LiveView
- High‑fidelity dashboards & forms
- Real‑time UX patterns
- Production tips to keep styles fast & maintainable
Build interfaces that are functional and exceptional—without slowing down.
Top comments (0)