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?
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!
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.
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.
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.
npm install -D tailwindcss postcss-cli postcss-elm-css-tailwind
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
@tailwind base;
@tailwind components;
@tailwind utilities;
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).
const postcssElmCssTailwind = require("postcss-elm-css-tailwind");
module.exports = {
plugins: [
require("tailwindcss")("./tailwind.config.js"),
postcssElmCssTailwind(),
],
};
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
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.
postcss-elm-css-tailwind generates a couple things for you:
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
)
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" ]
]
Media-query/breakpoint definitions are not generated as discrete funtions. Instead, a new module TW/Breakpoints.elm
is generated with a new opaque Breakpoint
type 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!"
]
]
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!
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
Coming Soon!
- 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 :)
- 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.