Canonicalizes Tailwind CSS utility classes in HEEx templates via
mix format.
Delegates to the tailwindcss CLI's canonicalize --stream
subcommand, which sorts classes, normalizes utilities to their
canonical form, and collapses duplicates. Powered by the same
Tailwind CSS engine as the
Prettier plugin.
- mr-4 custom-btn flex ml-[1rem] flex
+ custom-btn mx-4 flexUnknown classes are preserved and sorted to the front.
Requirements
- Elixir ~> 1.18
- Phoenix LiveView ~> 1.1 (for
attribute_formatterssupport) - The
tailwindcssCLI >= 4.2.2 (first version withcanonicalize)
The :tailwind package's default CLI version may lag this
requirement. If startup reports an older Tailwind version, set
config :tailwind, version: "4.2.2" (or newer) and run
mix tailwind.install.
Setup
Add canonical_tailwind to your dependencies:
# mix.exs
defp deps do
[
{:canonical_tailwind, "~> 0.1.0", only: [:dev, :test], runtime: false}
]
endThen in .formatter.exs, add attribute_formatters alongside your
existing HEEx formatter plugin:
# .formatter.exs
[
plugins: [Phoenix.LiveView.HTMLFormatter],
attribute_formatters: %{class: CanonicalTailwind},
# ...
]Now mix format automatically canonicalizes Tailwind classes in
class attributes.
Editor usage
If your editor formats via an LSP (like Expert or ElixirLS), the first
format-on-save after starting the editor will take a few seconds while
the tailwindcss CLI processes start up. Subsequent saves are near
instant.
Configuration
If you have the :tailwind hex
package set up with a single profile (the default for Phoenix
projects), everything is detected automatically — no configuration
needed.
Multiple tailwind profiles
If your project has multiple tailwind profiles, specify which one to use:
# .formatter.exs
[
plugins: [Phoenix.LiveView.HTMLFormatter],
attribute_formatters: %{class: CanonicalTailwind},
canonical_tailwind: [profile: :app],
# ...
]Pool size
CanonicalTailwind runs a pool of tailwindcss CLI processes to
parallelize mix format. The default is 6. Smaller projects may
benefit from fewer (less startup cost), larger projects from more (up
to your CPU core count).
# .formatter.exs
[
plugins: [Phoenix.LiveView.HTMLFormatter],
attribute_formatters: %{class: CanonicalTailwind},
canonical_tailwind: [pool_size: 3],
]Timeout
The tailwindcss CLI needs to initialize before it can respond to
its first request. On slower CI machines or larger projects, this can
exceed the default timeout of 30 seconds. Adjust with :timeout:
# .formatter.exs
[
plugins: [Phoenix.LiveView.HTMLFormatter],
attribute_formatters: %{class: CanonicalTailwind},
canonical_tailwind: [timeout: 60_000],
]Custom binary
If you're not using the :tailwind hex package, provide the path to
the CLI binary and optionally a CSS entrypoint. The CLI needs your
CSS entrypoint to resolve @theme customizations and plugins when
determining canonical forms.
:binary— path to thetailwindcssexecutable, relative to:cd:cd— working directory for the CLI process (defaults to the project root):input— CSS entrypoint, relative to:cd
# .formatter.exs
[
plugins: [Phoenix.LiveView.HTMLFormatter],
attribute_formatters: %{class: CanonicalTailwind},
canonical_tailwind: [
binary: "node_modules/.bin/tailwindcss",
input: "css/app.css",
cd: Path.expand("assets", __DIR__)
],
# ...
]Other attributes
The attribute_formatters key maps attribute names to formatters, so
any attribute holding Tailwind classes can be canonicalized. Register
each one the same way as class:
# .formatter.exs
[
plugins: [Phoenix.LiveView.HTMLFormatter],
attribute_formatters: %{class: CanonicalTailwind, "data-class": CanonicalTailwind},
# ...
]Umbrella projects
A single mix format run shares one pool of CLI processes across every
app it formats, so every app that uses CanonicalTailwind must resolve to
the same configuration: identical canonical_tailwind options and the
same tailwind profile (set :profile explicitly if the apps would
otherwise auto-detect different ones). A configuration that drifts after
the pool starts raises a clear error rather than silently formatting
against stale config.
If your apps genuinely need different configurations, run mix format in
each app separately so each gets its own pool.
Background
Built by a contributor to
TailwindFormatter,
attribute_formatters,
and
canonicalize --stream.