Generate your Tailwind utilities as elm-css functions

I created a PostCSS plugin that will take your Tailwind configuration and generate elm-css functions. This is similar to monty5811/postcss-elm-tailwind (and actually leveraged some foundational code from it) but instead of creating Html.Attribute it generates elm-css Style types.

The main benefit is that by leveraging elm-css, you can take advantage of Elm’s function-level dead code elimination. Tailwind generates A LOT of css. Their documentation directs you to use PurgeCSS to help keep your bundle size to the classes that you use. But with the Elm compiler, you get that automatically, which is pretty cool.

Some other cool things:

  • Most of the styles are generating actual elm-css functions and not just using the Css.property escape hatch. This means I do get some validation on the correctness of the styles via the Elm compiler. Of course this is not foolproof, but it’s a good gut check
  • As with everything in Elm, composability is key. This mean you can assemble your utilities into reusable complex groupings, sort of like the Tailwind @apply at rule.
  • I run elm-format on the generated output. Not super important, but I think it’s a nice little touch :slight_smile:
  • npm run test generates the output, runs the elm compiler on it, then uses Jest snapshot testing to help see if I broke anything.

A good chunk of standard utilities generate and compile. I have documented the known limitations at this point, but hope to get some of them figured out. The JS code is definitely a tad messy. Definitely some cleanup in the near future so be kind if you dig into the JS :wink:

Go check it out, and let me know what you think!

11 Likes

Really awesome ! Thank you for that :slight_smile: I have been looking for something like this. One thing: Maybe you could provide some practical, hosted intro examples ?

Edit: Maybe I could do that too

1 Like

Thanks!

I absolutely plan on fleshing out the documentation and examples a bit. Although, any contribution would be welcome! :slight_smile:

Before I do that I realize now of a bit of a change I need to make. The pure number of functions generated (over 100K lines of code) is killing the tooling, like elm-format, in VS Code. Not a good developer experience.

I realize that there is no real need to generate the breakpoint/media query based variations based purely on function name, which is what starts to cause the explosion of rules since Tailwind generates a duplicate rule for each breakpoint. I think it would be much smarter to leverage some of the preprocessing with elm-css. My plan is to extract the defined breakpoints into an opaque type so you can use them like:

TW.atBreakpoint small bg_blue_200

This will reduce the generated code significantly, but also give you named and reusable breakpoint types to pass around to other elm-css functions.

2 Likes

If anyone is interested, I just published a pretty big change for 0.2.0. As mentioned earlier, the sheer number of duplicate functions that are generated due to media query definitions was causing big problems with tooling like elm-format. Since elm-css is a pre-processor, I thought we could do something a little better.

  • All base utilities are now generated in a TW/Utilities.elm module. These do not include the media-query variants, just the base + pseudo selectors like active/hover.
  • A new TW/Breakpoints.elm module is generated that includes a new Breakpoint opaque type and constructor functions for each of your breakpoints. This also includes a function to leverage the utilities with the signature:
atBreakpoint : List ( Breakpoint, Css.Style ) -> Css.Style
atBreakpoint styles =

This guarantees that each media query is generated in the right order so you don’t get rules that override each other due to their definition order in the generated stylesheet from elm-css. You can use it like this:

    div [ css [ TW.bg_purple_200, atBreakpoint [ ( sm, TW.bg_red_800 ), ( lg, TW.bg_green_200 ) ] ] ]
        [ div []
            [ button [ css [ buttonStyle ] ] [ text "Button" ]
            ]
        ]

Sorry for such a big change right away, but I realized that if people can’t use elm-format in their projects due to this, no one would find this very useful.

Hope this is useful to some of you! Feedback is always welcome!

3 Likes

Nice work - I’m using some of the features, well plugins that work with 1.5 of tailwindcss, so I’m stuck with the css version for now - I have enjoyed monty5811’s package in the past and I really like what you are doing here. I look forward to using it soon.

1 Like

Thanks! Which 1.5 plugins are you using? I’ll add it to my list of things I’d like to work on support for and hopefully knock it out pretty quick so you can give it a spin!

On the current project the typography plugin that supplies “prose” - I honestly have no idea if it uses features that only exist in 1.5 or if it is simply something they put together for that release - for me, it provides a lazy way to make markup look good. Don’t push on my account - I’d prefer to work with your package, but I am still productive for now and able to wait.

1 Like

I saw the release of the “prose” plugin. Been interested to check it out myself! That one may be slightly trickier, as it isn’t just a simple class to declaration matchup like the majority of Tailwind is, but I definitely do want to support it. I think it would provide a nice way to style markdown made with elm-pages.

I made a GitHub issue if you would like to track my progress!

This is wonderful. Nice work!

1 Like

I got the “Space Between” helpers in my latest release. While doing that, I looked at what it would take to get the typography plugin/prose to work. Unfortunately they do a bit more magic nesting of selectors, which means I would have to do a little work translating those, since they are not such a simple utility class.

But, that did lead me to an npm package that should give me the lego blocks I need to parse out advanced CSS selector details (https://www.npmjs.com/package/css-selector-parser)

So given some free time, I may be able to get that prose thing going for you :slight_smile:

Cool - I did have a play with your package - It does certainly seem an excellent addition to elm-css, and a very neat solution to providing a more useable version of type-safe styling. I do find elm-css hideous to get into. It has always been the case that once I have got something working it then seems obvious, but it will have taken me hours of frustration to get there. So great stuff. My only glitch was with ‘container’ -

The .container class sets the max-width of an element to match the min-width of the current breakpoint.

Maybe its the breakpoint bit - but the effect was null using the package when the same combination of classes behave very nicely using ‘raw’ tailwindcss. The other issue was that I couldn’t put classes on svg - an elm-css thing since they have to be converted to styled html before elm-css can work with them.

1 Like

Thanks for the feedback!

Ah shoot, I am sure there are a decent number of quirks that are hiding. The compile is a gut check, but it doesn’t mean a declaration didn’t make it over or something. I will investigate!

I created another GitHub issue if you would like to follow it along!

I will also ponder the SVG limitation. I feel like there is something that could bridge the gap.

One weakness of Tailwind proper is that it doesn’t handle :before and :after pseudo-elements without resorting to third-party plug-ins.

As with “:hover”, your library can piggyback on the elm-css library to make this much easier. See this GitHub issue, for the workaround* necessary.

Just beginning to explore the library but it seems like you’ve taken two killer apps, tailwind and elm-css, and made an even more beautiful baby with them.

PS: Perhaps “workaround” isn’t the right term. The Github issue links to a broken Ellie with the solution for a fix. But that solution may only be relevant to the specific use case.

1 Like

I figured out what is wrong with the container utility. Would love feedback on which direction to go with it:

The container class is a bit different than the other breakpoint-based utilities. The raw Tailwind for example is this:

@media (min-width: 640px) {
  .container {
    max-width: 640px;
  }
}

In particular is uses the breakpoint pixel size inside of the declaration. Since we don’t generate the breakpoint based utilities, if you use the Breakpoint utility Tw.Breakpoints.atBreakpoint [(TW.Breakpoints.lg, TW.container)] you just end up with a width: 100% and not one that applies the Breakpoint pixels…which is sort of useless.

Two solutions come to mind:

  • The atBreakpoint helper could be smarter and handle the case that someone uses container and apply the breakpoint px rule correctly - I like this from an API perspective, as it behaves as you would like it to. The function is a little magical at that point though because it replaces the style you pass in with a new one.

  • Create a new Breakpoint utility like :

container : Maybe Breakpoint -> Css.Style

Where if you pass in a breakpoint, it applies it, otherwise you get the default container width: 100%

I like the explicit behavior or this, but it may not be super obvious that you can’t use the utility as you would expect, you need to know to use some extra helper:

div [ css [TW.Breakpoints.container (Just lg) ] ]
            [ div [ css [ TW.space_y_32 ] ]
                [ div [  ] [ text "div" ]      ]
            ]

Thoughts?

Thanks for the kind words! and also thanks for the links!

If you try it out, feel free to let me know of any pain points or bugs you hit. Also don’t be afraid to submit GitHub issues or just reach out to me directly. I’ve been really liking how it’s coming together, but I want to make sure it works well for everyone who is interested!

I wanted to let everyone know who may be following that 0.4.1 is released!

This adds support for CSS Grid, and also fixes the issue reported by @thetechnaddict about the container bug. https://github.com/justinrassier/postcss-elm-css-tailwind/releases

Please let me know if I’m breaking any etiquette by continuing to post on this thread. I want to keep people informed if they care, but also don’t want to spam people.

Dunno about etiquette - but I appreciate the updates :slight_smile:

1 Like

I absolutely appreciate updates, too!

I think this package is a great idea. I’ve hoped for something like this with tailwind.

When trying out this package I’ve had some stumbling blocks:

  • When you run postcss on a .css file with no @tailwind directives, you’ll end up with a syntactically incorrect TW/Breakpoints.elm file, because you’ve got 0 breakpoints.
  • I migrated from a normal tailwindcss build with webpack to this library, so I was changing my existing postcss config that I was using for all my css. This resulted in postcss-elm-css-tailwind to be triggered multiple times, sometimes on css files without @tailwind directives. I took me a little to understand that this was not the way this library was ment to be used.
  • I’d like to have some configuration options, or at least different defaults. I don’t like putting generated code in my src/ directory, so I usually put generated elm code into gen/ instead, similar to elm-pages. Apart form that I’d love to have camel case and Tailwind instead of TW, but that’s just personal preference, I’m fine with those being hardcoded defaults instead.

All in all great package! Can’t wait to use it :slight_smile:

1 Like

Thanks, @Philipp_Krueger, I also hit the “0 breakpoints” bug but assumed the fault lay in my Parcel configuration (or lack thereof) as I’ve only used Webpack in the past.

I think the naming (and location) of the folder holding the generated files should be configurable, as with elm-graphql. And, yes, my own preference would be to keep it out of the “src” directory.

For those of us using multiple libraries that generate files (elm-spa, elm-graphql, postcss-elm-css-tailwind, etc), we may want to create a parent folder for them all.

1 Like

Dang, I can’t believe I didn’t think about usage with different bundlers. Sorry about that! The tricky part about that one is in PostCSS you are given just the AST after previous plugins are done transforming it. So I have to find a way to know if the file being worked on is your Tailwind file or just another CSS file. I have an idea for it though…

You have convinced me to get my act together and get some configuration options available, it actually is what I think I’ll need to accomplish what I mentioned above! It’s been a while since I’ve configured Webpack, and since there are infinite ways to do so, I am wondering if you are able to offer up a webpack config that recreates your issue? That way I can make sure it’s solved for your use case!

That goes for you as well @creminology. I haven’t used Parcel (although I’ve always wanted to try). Any way I can verify that I solve your problem would be great!

I created two GitHub issues for you to follow respectively


Thanks for all the feedback!

1 Like