Getting Tailwind to Work with Elm Book

Trying to help build a design system at work in my spare time; no clue if it will go anywhere but it’s fun regardless. I asked the Elm Slack group what the equivalent of React Storybook is in Elm land. Specifically, I wanted a way to build a documentation website like Vuepress with the ability to host native Elm code to showcase components. They pointed me to Elm Book. While Elm Book has built-in theming capabilities, I needed CSS control over my components. While they support elm-css, I wanted the ability to use TailwindCSS. The Elm libraries haven’t kept up with Tailwind’s changes, which is fine; writing raw Tailwind CSS on Elm HTML functions is easy and co-located with the component you’re styling.

However, getting it to work in elm-live, which elm-book wraps, was a bit challenging. I wanted to layout how to get this to work in case you’d like to use Tailwind or your own CSS framework.

Why Elm-Live?

Almost all web projects nowadays require 3 important things:

  1. Run a local web server so you can see your website as you write code
  2. Refresh the browser window when you change code so you immediately see changes
  3. Compile, lint, and/or bundle your code changes

Most front-end frameworks nowadays include these features as part of their CLI’s such as Create React App, Angular’s ng-cli, etc. For those that don’t, many will use a combination of some type of bundler like Parcel, Rollup, or Webpack and a browser refresher like livereload. This enables you to write code, save it, and immediately see the results. This in turn leads to fast feedback as you iterate all day in this build loop. The native Elm Reactor doesn’t offer this ability and elm-live fits the bill as a small Node.js library to enable this.

Two Terminals And package.json

For good or ill, I’m using the 2 Terminal approach, where you start your webserver in one Terminal, and your TailwindCSS CLI watcher in another. This allows elm-live to detect you’ve changed Elm, HTML, JavaScript, and CSS files and have it recompile your code and refresh the browser. The Tailwind CLI will detect you’ve changed HTML in CSS and will recompile your CSS. Both the Elm compiler and Tailwind’s version 3 CLI are fast enough that you don’t often get race conditions; meaning, your CSS is compiled first, then Elm, then the browser refreshes. If the Elm compiles first, then the CSS, the browser refresh is usually slow enough that the application still loads all the latest code.

The proper thing to do would have Tailwind finish first, then allow elm-live to run the elm compiler, but getting that to work requires a lot of engineering effort. The Elm Slack gave some great recommendations, but it seemed tricky since the elm-live library would have to “know” to wait. A challenge for another time.

Configuring Tailwind CLI

Modifying your package.json to support Tailwind is basically a copy pasta from the documentation. In your package.json scripts, you just add:

"css:watch": "npx tailwindcss -i ./src/tailwind.css -o static/tailwind.build.css --watch"

Then, you run npm run css:watch and it’ll start recompiling your tailwind.build.css every time your CSS changes. The tailwind.config.js just needs to include Elm files in it, not just html and js which is the default:

module.exports = {
  content: ['./src/**/*.{html,elm,js{']
}

Now you can write Tailwind CSS in your Elm functions, and it’ll trigger Tailwind to recompile the tailwind.build.css with the Tailwind CSS styles you’ve used.

Configuring Elm-Live

Elm Book comes with elm-live built-in, but you don’t have to use that if you want more control. Instead, we’ll add another script to your package.json‘s scripts that starts elm-live to watch your Elm code, and recompile + refresh the browser.

"start": "elm-live src/Main.elm --pushstate --startpage=./static/index.html --dir=./static -- --output=static/elm.js

Now when you run npm start, it’ll do a bunch of good things:

  • recompile your Elm code
  • if successful, it’ll refresh the browser, else who a compiler error both in Terminal and in the browser
  • utilize our own index.html file vs an elm-book generated one
  • compile our Elm code to the static directory
  • read all files from the static directory
  • handle deep linking when you refresh the browser

That last one is why I wrote this blog post. That took me hours to get the right combination of directories, file names, and configs correct.

Configuring index.html

Your index.html belongs in the static directory, not src. In it, we’ll put some important tags, which I’ll highlight below the code:

<html>
<head>
  <link href="/tailwind.build.css" rel="stylesheet" type="text/css"></link>
<body>
  <main></main>
  <script src="./elm.js"></script>
  <script>
    var app = Elm.Main.init({ node: document.querySelector('main') })
  </script>
<body>
</html>

Note both tailwind.build.css and elm.js have a forward slash in front. This tells the webpage “look in the root/top level directory”. Since both files are next to index.html, it finds them. Sometimes you can get away with a dot slash like ./ or simply just the file names with no proceeding slash, but the web server gets confused that it’s running in root, but serving files from static. This combination makes it work.

Your Elm code and tailwind.css files can reside in src. The only caveat is I added static/elm.js and static/tailwind.build.css to .gitignore as I don’t want compiled assets checked into source control as they change often and that’s sometime a Continuous Integration server should build as an artifact, store, and deploy.

Conclusion

Now when writing elm-book chapters, you can use Tailwind CSS styles in your Elm html functions and components:

h1 [ class "text-3xl font-bold underline" ] [ text "Sup" ]