Tailwind JIT (Just In Time Engine) and Phoenix LiveView

Hello Everyone! :blush:

I’m currently exploring tailwind and its ability to create custom utilities and animations. I don’t know if i’m wrong here, so please correct me if so, but it seems that phoenix is not using the JIT (Just In Time Engine) within Tailwind. Or perhaps my understanding of the JIT engine is flawed.

I’m creating my custom animations in the tailwind.config.js file as outlined in the Tailwind docs Animation - Tailwind CSS. I then use these inline within the html tags inside my LiveView.
It was my understanding that to avoid a large app.css file, the JIT engine processes and sends the css only when the user is on the page where that inline styling resides. However, even when on the landing page the app.css along with all the custom animations (and other TW utilites) are present and being shipped to the client even though they are not interacting with or on the page where those animations are being used. This concerns me because I plan on making many custom animations and I neither want to ship a giant app.css file nor let the user see my custom animations unless auth’d and on that specific page.

If anyone can enlighten me as to why this is, or highlight why i’m wrong and steer me in the right direction on this one, it would be greatly appreciated!

Many thanks,

Liam : )

Tailwind is creating one css file for your whole project. It doesn’t create separate bundles for every file.

The css file should also be really small even if you use it accross pages. You can read more about it here: Optimizing for Production - Tailwind CSS


There is a project, twind, that ships the compiler instead of the CSS and compiles on the fly.

1 Like

This is not necessarily true, is pretty easy to split files, I do it for separate admin and main site css bundles (also do it with esbuild which is dramatically more impactful since js libs can make the bundle grow up very fast).

@Liam_or what tailwind JIT does is inspect your source files for tailwind classes and generate a css file on the fly, but this is during development only, when deploying you run tailwind minifying it and then deploy the css generated file.

Also, css files can’t be obscured, every major modern browser has introspection tools that allow you to see exactly which properties are being applied to any element in the page, if you want to restrict a logged out user of the animations you should handle this in the logic of your code, not in your css.

1 Like

You mean there are workarounds?
Tailwind only produces one css file. So either you’re using multiple tailwind configs or you use esbuild to split the CSS (which I don’t know if it can?). Or I’m missing something else?

1 Like

You’re right, multiple tailwind configs (and esbuild ones, where the admin loads everything all at once but the main bundle for the app load big dependencies on demand).

# config.exs
config :tailwind,
  version: "3.3.2",
  default: [
    args: ~w(
    cd: Path.expand("../assets", __DIR__)
  admin: [
    args: ~w(
    cd: Path.expand("../assets", __DIR__)

# dev.exs
  watchers: [
    tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]},
    tailwind: {Tailwind, :install_and_run, [:admin, ~w(--watch)]}

# aliases in mix.exs
 "assets.deploy": [
        "tailwind default --minify",
        "tailwind admin --minify",

IIRC this is all that is needed.


Hello! Thanks so much for all your comments. I Really appreciate you sharing your knowledge on this :blush:

@thomas.fortes how do you conditionally serve those tailwind configs so that they are only served to either an admin or a default user? Could you also go into a little bit more detail about how you conditionally serve esbuild bundles? I would like to do a mini experiment with this approach in a LiveView app.

I would love for phoenix LiveView to one day have easy code splitting and bundle serving like how SveltKit uses vite to only serve JS and CSS when needed.

I don’t, I use different root layouts for the admin dashboard and the frontend and include the correct css in each layout, if I really had to serve different files to the same root layout I would probably use a Plug or on_mount to know if the user is an admin or a default user and serve it conditionally like any other conditional.

As for the esbuild, in config.exs:

config :esbuild,
  version: "0.18.0",
  default: [
    args: ~w(js/app.js js/video-player.js
      --chunk-names=chunks/[name]-[hash] --splitting --format=esm --bundle --target=es2017
      --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}

You can see that I’m passing two files, app.js and video-player.js to the esbuild command, then in the video-player.js you can import your big deps normally and import it dynamically in a hook in the app.js file.

// app.js
Hooks.VideoPlayer = {
    mounted() {
            ({ setupVideoPlayer }) => {

This way the big video player dependency will be loaded only when you hit a page that uses the hook.

1 Like

Awesome! :blush: Thanks @thomas.fortes, really appreciate you sharing your knowledge.