Slim-style templates for Phoenix LiveView.
Write this:
div.container
h1.title = @page_title
= for item <- @items do
.card phx-click="select" phx-value-id={item.id}
span.name = item.name
Get this:
<div class="container">
<h1 class="title"><%= @page_title %></h1>
<%= for item <- @items do %>
<.card phx-click="select" phx-value-id={item.id}>
<span class="name"><%= item.name %></span>
</.card>
<% end %>
</div>Two reasons.
Closing tags are unnecessary noise. Indentation already communicates structure. Every </div> is a line that exists only for the parser, not for you. Slim figured this out in 2010. HEEx templates are verbose — a typical LiveView template is 40-60% closing tags and boilerplate. Slime cuts that in half.
Agents read templates too. Every AI coding agent — Claude Code, Cursor, Copilot — parses your templates into its context window. Tokens spent on </div> are tokens not spent on understanding your logic. In a world where AI agents write and modify templates constantly, concise source format isn't just preference — it's throughput. Elixir is already one of the best languages for agent-assisted development (pattern matching, pipelines, explicit data flow). SlimeHEEx extends that advantage to the template layer.
SlimeHEEx is a preprocessor. Write .slime files, run mix slime, get .heex files. Phoenix picks them up normally. You never fight the framework — you just have a nicer authoring layer.
mix slime # Convert all .slime files under lib/
mix slime path/to/file.slime # Convert a specific filediv / <div></div>
p Hello world / <p>Hello world</p>
h1.title Page Title / <h1 class="title">Page Title</h1>
div.foo.bar / <div class="foo bar"></div>
div#main / <div id="main"></div>
div#main.foo.bar / <div id="main" class="foo bar"></div>
.container.mx-auto / implicit div: <div class="container mx-auto"></div>
a href="/home" Home / string attributes
div class={@class} / elixir expression attributes
button phx-click="save" Save / phoenix event attributes
div :if={@show} / conditional rendering
input type="text" name="q" / void elements self-close
Indentation defines structure. Two spaces per level.
div.outer
div.inner
p Deeply nested
= outputs the expression. - executes without output.
= @user.name / <%= @user.name %>
= for item <- @items do / block expressions auto-close
div = item.name
= if @show do
div.visible Content
- else
div.hidden Nothing
Trailing = expr on a tag outputs the expression inside the tag.
span.name = user.name / <span class="name"><%= user.name %></span>
.component_name (with underscores) renders as a function component.
.icon name="hero-star" / <.icon name="hero-star" />
.modal title="Settings" / <.modal title="Settings">
p Content / <p>Content</p>
/ </.modal>
.table rows={@users}
:col :let={user} label="Name"
span = user.name
/ This comment is stripped from output
/! This becomes an HTML comment / <%!-- This becomes an HTML comment --%>
| This text passes through as-is
doctype html / <!DOCTYPE html>
- Tailwind fractional/responsive classes like
py-1.5,sm:grid-cols-4,border-emerald-700/50conflict with dot notation. Use theclass="..."attribute form for these. Simple classes like.font-bold.text-smwork fine with dots. - No multiline attribute support yet.
- No file watcher — manual
mix slimefor now. - No source maps — errors point at generated .heex lines.
Add to your mix.exs:
def deps do
[
{:slime_heex, "~> 0.1.0"}
]
endOr just copy lib/slime_heex/ into your project. It's three files.
Spike. 28 tests passing. Used in production at Orca. Contributions welcome.