Skip to content

justinrassier/postcss-elm-css-tailwind

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Elm CSS Tailwind

Build, Test and maybe Publish

Generate elm-css versions of your Tailwind utilities.

Tailwind CSS is an amazing utility-first approach to CSS which enables rapid development within predefined color, spacing, breakpoint systems.

elm-css is a beautiful package that allows you to write compiler-guaranteed style sheets with pure Elm.

What if we could marry the benefits of both systems? What if our Tailwind utilities were just elm-css Style types?

Elm CSS Tailwind is a postcss plugin that takes your Tailwind config setup and spits out a pre-generated Elm module that contains all of your Tailwind utilities as elm-css Style types which can be used directly with elm-css.

Why would we want to do this?

Compiler Help

Tailwind by default is just CSS classes. This is fine and dandy, but we in the Elm world love our compiler help. With everything defined as a elm-css Style, you cannot write a Tailwind utility that doesn't exist!

Purged CSS without PurgeCSS

By default, Tailwind generates A LOT of CSS. They have done a great job at making it work well with compression, but by default you end up with a pretty large CSS file that you have to ship to your client.

Tailwind recommends that you setup PurgeCSS as part of your build, and actually started shipping with a purge option as of 1.4.1

This is fine and all, but this feels like an extra step. Elm has function-level dead code elmination built in by default. Therefor by writing in pure Elm with elm-css, it means we only bundle the styles that are used right out of the box! No need to purge your stylesheet.

Composable

Those who are first introduced to Tailwind have a pretty visceral reaction to seeing something like this in their markup:

<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
  Button
</button>

What if you want all your buttons to look the same? Do you have to copy all 7 CSS classes around? One argument is to extract that markup into your framework of choice for reuse, which is one approach. Another approach is to use the Tailwind ability to extract components. To us in Elm, this looks very much like composing functions. So instead of using the Tailwind means of composing classes, we can do something like this:

buttonStyle: Css.Style
buttonStyle = 
     Css.batch [ TW.bg_blue_500, Css.hover [ TW.bg_blue_700 ], TW.text_white, TW.font_bold, TW.py_2, TW.px_4, TW.rounded ]

This now becomes our reusable style that is comprised of all the base Tailwind utilities.

Getting Started

Note: This is still pretty early. A large amount of the utilities are generated and compile, but have not been verified. Feel free to report an issue and I'll dig into what property isn't generating correctly.

Install tailwind, postcss and the postcss plugin

npm install -D tailwindcss postcss-cli postcss-elm-css-tailwind

Install elm/regex into you Elm dependencies

The generated Breakpoint helpers (see below) have a dependency on elm/regex Perhaps a companion Elm package would be more appropriate, but for now you manually have to install this dependency.

elm install elm/regex

Create a base tailwind.css file

@tailwind base;

@tailwind components;

@tailwind utilities;

Create a tailwind.config.js file (optional)

All core plugins work out of the box, so you don't actually need a config file. If you like to have a basic one ready for customization, you can add the following to your tailwind.config.js

module.exports = {
  theme: {},
  variants: {},
  plugins: [],
};

Note: If you have a heavily customized Tailwind.config file, things may not work properly. Initial development was based off of the default config. The goal is to have robust support of all the ways you can configure Tailwind. That being said, basic configs should work (e.g. color names and definitions, breakpoint sizes).

Create a postcss.config.js file

const postcssElmCssTailwind = require("postcss-elm-css-tailwind");

module.exports = {
  plugins: [
    require("tailwindcss")("./tailwind.config.js"),
    postcssElmCssTailwind(),
  ],
};

Run PostCSS CLI to generate your files

This will generate a TW.elm file (with a corresponding TW module) inside of your src/ directory. Right now there isn't a way to configure the output, but that should be coming soon

PostCSS is going to still kick out a generated CSS file, you can either leverage it, or discard it.

 npx postcss -o dist/main.css tailwind.css

Include CSS normalize file

Tailwind operates and generates a built in normalize.css file. This plugin only generates the utilities, you will still want to include a normalize CSS to get the expected behavior. For convenience, you can grab the tailwind-base.css file from this repo directly, which is just the @tailwind base output.

Use your utilities!

postcss-elm-css-tailwind generates a couple things for you:

TW/Utilities.elm

These are all of your base Tailwind utility classes. Their naming convention is changed to work with Elm as follows:

  • The '-' is replaced with '_' (e.g. mb_4, bg_blue_400)
  • Negative margins are prefixed with neg (e.g. neg_m_4)
What about pseudo selctors like :hover or :active?

Turns out we don't really need them in the same way you do in traditional Tailwind. It's true this is a bit of a departure, but I don't think it will feel all that bad, even if you are Tailwind purest. Elm CSS comes with function Css.hover or Css.active that play nice with your utility classes. These compose well and also reduce the amount of generated code, therefore helping your tooling like elm-format of elm-analyze out.

Example:

 div [ css [ Css.hover[TW.space_y_32] ] ]
    [ div [] [ text "div content goes here" ]
    , div [] [ text "div content goes here" ]
    , div [] [ text "div content goes here" ]
    ]

TW/Breakpoints.elm

Media-query/breakpoint definitions are not generated as discrete funtions. Instead, a new module TW/Breakpoints.elm is generated with a new opaque Breakpointtype and a contruction function for each of your breakpoint names. (e.g. sm, md, lg, xl) along with the following utility function:

atBreakpoint : List ( Breakpoint, Css.Style ) -> Css.Style
atBreakpoint styles =

Because of the way elm-css processes the rules, this function guarantees that if you have multiple breakpoint definitions, that they will be applied in the correct order to not be overwritten by each other:

view : Model -> Html Msg
view model =
    div [ css [ TW.bg_purple_200, atBreakpoint [ ( sm, TW.bg_red_800 ), ( lg, TW.bg_green_200 ) ] ] ]
        [ div []
            [ text "Hello World!"
            ]
        ]

Configuration

Don't like the defaults? There are some configuration options for you to leverage:

    postcssElmCssTailwind({
      //The name of your base Tailwind.css file (see below about working with bundlers) 
      baseTailwindCSS: "./tailwind.css", 
      // The root directory where the code will be generated
      rootOutputDir: "./gen", // the generated output directory 
      // The root module name for both the Utilities and Breakpoints module
      rootModule: "Tailwind",
    }),

This will generate:

  • /gen/Tailwind/Utilities.elm
  • /gen/Tailwind/Breakpoints.elm

The goal is to keep configuration as simmple as possible. If more configuration is needed, reach out to me or put in a GitHub issue!

Working with bundlers

When you work with a bundler like Webpack or Parcel, you may end up sending CSS through PostCSS that is not your Tailwind utilities. To make sure invalid Elm is not generated when this happens, postcss-elm-css-tailwind needs to know what the name of the file that contains your Tailwind.css at-rules (e.g. @tailwind base)

Example of config with Parcel

    postcssElmCssTailwind({
      baseTailwindCSS: "./tailwind.pcss", // Parcel file extension for PurgeCSS
      rootOutputDir: "./gen",
      rootModule: "Tailwind",
    })

Any CSS file or PostCSS file that Parcel picks up will be ignored unless it matches the defined baseTailwindCSS configuration.

Note: The default configuration options work, but as soon as your bundler works with multiple CSS files, you will need to define the baseTailwindCSS configuration option to end up with valid Elm

Developing

Coming Soon!

Supported

  • Everything compiles with a default config with Tailwind 1.6. I tried to get as much into raw elm-css to help guarantee a valid stylesheet. I sometimes had use the escape hatch and use Css.property when either the support (or ambition) was lacking. This shouldn't cause any problem considering Tailwind is the source of truth for the properties, so they should be correct :)

Future enhancements

  • Add auto prefixer support. Adding it now causes duplicate function definitions that I need to determine how to group.
  • Include the normalize CSS stylesheet as part of the package. Tailwind assumes you leverage the CSS reset
  • Work with custom Tailwind configuration. Right now everything is developed with a default Tailwind config
  • Extract color palette definitions into exported values or maybe opaque types

Shout out to monty5811/postcss-elm-tailwind: They did some great foundational work that I leveraged. I attempted to fit my new code in with theirs, but the data structures I needed were a little more complex/nested and would require a full refactor of their code. You will still see some remnants of their code lingering around.