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

kevinlang
Hey all, We have made an Ecto3 Adapter for SQLite3, ecto_sqlite3! We have successfully on-boarded the full suite of integration tests (...
New
pkrawat1
Presenting Aviacommerce, open source e-commerce platform in Elixir Aviacommerce is an open source e-commerce platform in Elixir. We at...
New
Crowdhailer
I have been updating a library that allows you to pipe between functions that use the erlang result tuple convention. Assuming you have ...
New
riverrun
I’ve just released version 3 of Comeonin, a password hashing library. The following small changes have been made: changes to the NIF c...
New
kelvinst
Hey everyone! Well, we made this lib a while ago and now we decided to finally go out and public with it! It’s a tool for creating and m...
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 12673 134
New
OvermindDL1
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML...
132 14085 106
New
handnot2
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application. This library uses Erlang esaml to provide plug enabl...
New
wfgilman
I’ve cleaned up and open sourced three financial libraries I was using for my company. They are bindings for the APIs of these three comp...
New
mattludwigs
Grizzly is a library for working with Z-Wave devices. Z-Wave is a low-frequency radio protocol for controlling smart home devices on a me...
New

Other popular topics Top

axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 48475 226
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 127536 1222
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement