aptinio

aptinio

canonical_tailwind - Tailwind class canonicalization via mix format

I just published canonical_tailwind, a formatter plugin that canonicalizes Tailwind CSS utility classes in HEEx templates.

- mr-4 custom-btn flex ml-[1rem] flex
+ custom-btn mx-4 flex

It delegates to the tailwindcss CLI’s new canonicalize subcommand — the same engine that powers the Prettier plugin. So you get sorting, normalization, and duplicate collapsing powered directly by the Tailwind CSS engine.

It hooks into mix format via Phoenix LiveView 1.1’s attribute_formatters API and works with LSP format-on-save (tested with Expert).

Setup is two lines — one dep, one formatter config:

# mix.exs
{:canonical_tailwind, "~> 0.1.0", only: [:dev, :test], runtime: false}
# .formatter.exs
[
  plugins: [Phoenix.LiveView.HTMLFormatter],
  attribute_formatters: %{class: CanonicalTailwind},
]

If you’re already using the :tailwind hex package, it detects your binary and profile automatically. Make sure your tailwind version in config/config.exs is at least 4.2.2 and run mix tailwind.install after changing it.

Requires Elixir ~> 1.18, Phoenix LiveView ~> 1.1, and tailwindcss CLI >= 4.2.2 (the first version with canonicalize). The canonicalize --stream flag that makes this possible was merged in tailwindcss#19796.

Hex: canonical_tailwind | Hex
Docs: canonical_tailwind v0.3.0 — Documentation

https://github.com/aptinio/canonical_tailwind

Most Liked

aptinio

aptinio

It was a long time coming. I first opened a PR for Phoenix LiveView asking the team to make HTMLFormatter.tokenize/1 public. José Valim came back with a better idea — attribute_formatters (phoenix_live_view#3781) — which gave formatters a proper hook into the template pipeline.

The initial implementation parsed Tailwind’s output CSS to derive the sort order. It worked, but I was never happy with it, especially with format-on-save enabled in an editor. The ideal solution was to hand the formatting off to Tailwind itself, and that became possible once the canonicalize subcommand landed. I contributed a --stream flag (tailwindcss#19796) so the CLI could stay alive as a long-running process, and after that the library was the easy part.

Hope you find it useful!

aptinio

aptinio

Just shipped 0.1.5. The headline: if format-on-save just wasn’t working for you under Expert or ElixirLS, 0.1.5 fixes it.

Also tightened pool reliability: workers that go missing now recover (long-running LSP sessions used to break after a crash), cold start is serialized to avoid mixed-config workers, and config drift after pool start raises a loud error instead of silently using stale config (a small breaking change for umbrella setups that depended on the old quiet behavior).

And if you haven’t tried canonical_tailwind yet, this is a good moment. The canonicalizer in tailwindcss has gotten noticeably smarter over the past few releases: combining text-* with leading-*, collapsing matching width/height into size-*, modernizing calc(var(--spacing)*…) into --spacing(…), and now in v4.3.0, migrating [&:has(…)] to has-[…] and preserving significant whitespace and original units in arbitrary values. canonical_tailwind delegates to the user’s installed binary, so all of that lands for you the moment you bump :tailwind.

Changelog: Changelog — canonical_tailwind v0.3.0

Hope you find it useful!

aptinio

aptinio

Just shipped 0.3.0. There’s a new ~TW sigil.

canonical_tailwind already canonicalizes class attributes in HEEx. But Tailwind classes often live in plain .ex code too: helper functions, module attributes, class-builder lists. A formatter can’t safely touch those on its own, since it can’t tell a Tailwind class string from any other string.

~TW lets you mark a string as a class list, so mix format canonicalizes it:

defp button_class, do: ~TW"px-4 py-2 inline-flex rounded-md bg-brand"
# becomes, on mix format:
defp button_class, do: ~TW"inline-flex rounded-md bg-brand px-4 py-2"

It works anywhere a string literal does. It’s opt-in per string, so you canonicalize the strings you want and leave the lists you’ve ordered by hand.

Add CanonicalTailwind to your formatter :plugins and import CanonicalTailwind.Sigil to use it.

Hope you find it useful!

Where Next?

Popular in Announcing Top

tmbb
I’ve published the first version of my Makeup library. It’s a syntax highlighter for Elixir in the spirit of Pygments, Currently it highl...
New
sasajuric
I’d like to announce a small library called boundaries. This is an experimental project which explores the idea of enforcing boundaries ...
New
mspanc
I am pleased to announce an initial release of the Membrane Framework - an Elixir-based framework with special focus on processing multim...
New
ostinelli
Let’s write a database! Well not really, but I think it’s a little sad that there doesn’t seem to be a simple in-memory distributed KV da...
New
dbern
I’m excited to announce that TaxJar has developed and open-sourced DateTimeParser. We developed it because we found a need to parse user ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
Qqwy
Hello everyone, I wrote a small library today called MapDiff. It returns a map listing the (smallest amount of) changes to get from map...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36432 110
New
benlime
LiveMotion enables high performance animations declared on the server and run on the client. As a follow up to my previous thread A libr...
New
trisolaran
Hi! :waving_hand: I would like to present LiveSelect, a little library that I wrote to easily add a dynamic selection input to your LV f...
198 10978 107
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39523 209
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36432 110
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement