United Kingdom: +44 (0)208 088 8978

Tailwind CSS with SAFE

This week Akash goes through how you can use Tailwind CSS with the SAFE stack

UI Frameworks are a great way to quickly build a sleek application, letting you focus on solving the core business needs rather than getting bogged down with how many pixels wide something should be. I still love writing custom CSS, but for client work it's often not worth going down the rabbit hole of creating an entire UI library.

Tailwind hits that sweetspot between having a UI library and handcrafted CSS. It achieves this by providing a tonne of utility classes that follow SRP. I think of it as 'someone a lot better than me at CSS has saved me the time of writing the classes I wanted to write', unlike when I've used UI frameworks in the past and it has felt like someone else's code and preferences.

Let's see how we can integrate this awesome tool with the SAFE Stack.

In this demo we will be using the full tailwind packages rather than downloading via a cdn, as that doesn't offer the full range of features

1) Create a new SAFE app

dotnet new SAFE

I am using the SAFE 3.0.0-beta004 template

2) Install dev dependencies

npm i -D tailwindcss postcss autoprefixer

3) Create tailwind.css

Create a folder called css in the Client project directory.

Inside that css folder, create a new file and name it tailwind.css.

Open the tailwind.css file and add:

@tailwind base;
@tailwind components;
@tailwind utilities;

4) Generate tailwind and postcss config files

Run this command in the Client project directory:

npx tailwindcss init -p

That should've generated two files the tailwind.config.js and postcss.config.js. You can think of the tailwind.config.js as the place you will need to change should you want to customise your tailwind setup.

5) Install and add postcss-loader

Install postcss-loader as a dev dependency.

npm i -D postcss-loader

I had to use version "4.2.0" of postcss-loader in order for the Client to compile, you may need to do the same in your package.json

In the webpack.config.js file, within the use section of the css rules, add 'postcss-loader' to the end.

It should look something like:

...
            {
                test: /\.(sass|scss|css)$/,
                use: [
                    isProduction
                        ? MiniCssExtractPlugin.loader
                        : 'style-loader',
                    'css-loader',
                    {
                        loader: 'sass-loader',
                        options: { implementation: require('sass') }
                    },
                    'postcss-loader'
                ],
            },
...

6) Using tailwind!

Within Index.fs in the Client directory we need to import our tailwind.css file.

open Fable.Core.JsInterop

importAll "./css/tailwind.css"

Check out the SAFE docs for more information about import statements

Let's try to use some tailwind classes.

let view (model: Model) (dispatch: Msg -> unit) =
    Html.button [
        prop.className "bg-blue-500 h-10 w-20 rounded text-gray-50"
        prop.text "Hi"
    ]

We should now see a pretty swanky button! 😎

7) Styling different states

Tailwind can generate a tonne of classes, some of which can be used to target particular states of an element. For example the hover or focus states. We can add these custom styles by prefixing the state:

hover:bg-blue-300

Let's start applying some of these to our button.

let view (model: Model) (dispatch: Msg -> unit) =
    Html.button [
        prop.className
            "bg-blue-500
             h-10 w-20
             rounded
             text-gray-50
             hover:bg-blue-300
             hover:text-gray-500
             focus:ring
             focus:ring-offset-2
             focus:ring-blue-300
             focus:ring-opacity-50"
        prop.text "Hi"
    ]

We should see a few changes:

  • The background is now lighter on hover 💡
  • The text is darker on hover 🕶
  • There is a blue ring around the button when the element is in focus 💍

8) Extending the tailwind config

Tailwind comes with a lot of behaviour out of the box, but sometimes we have a particular need to style things a certain way. You may, for example, want to change the colour of the button when it is clicked so that the user has some feedback.

Head over to the tailwind.config.js we created earlier, and within the variants.extend object we need to add

backgroundColor: ['active']

The config file should now look like:

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: { backgroundColor: ['active'] },
  },
  plugins: [],
}

We put it within the extend object, as opposed to the top level in the variants object, as that would override the existing setup and cause states like hover to no longer be applied

With this we should be able to add an active state to our button!

let view (model: Model) (dispatch: Msg -> unit) =
    Html.button [
        prop.className
            "bg-blue-500
             h-10 w-20
             rounded
             text-gray-50
             hover:bg-blue-300
             hover:text-gray-500
             focus:ring
             focus:ring-offset-2
             focus:ring-blue-300
             focus:ring-opacity-50
             active:bg-blue-700"
        prop.text "Hi"
    ]

9) CSS crazy!

Our tailwind classes are quickly highjacking our template and we've only got one button on the screen! So how can we extract this out and make it into a class we can use everywhere?

Let's copy the classes from our button, and head over to the tailwind.css file.

In between the components and the utilities add a normal CSS class .awesome-btn.

Before pasting all the tailwind classes we've copied from Index.fs, prepend them with @apply:

@tailwind base;
@tailwind components;

.awesome-btn {
    @apply
        bg-blue-500 h-10
        w-20
        rounded
        text-gray-50
        hover:bg-blue-300
        hover:text-gray-500
        focus:ring
        focus:ring-offset-2
        focus:ring-blue-300
        focus:ring-opacity-50
        active:bg-blue-900
}

@tailwind utilities;

The reason the class goes in between components and utlities is that we may need to update the utlities on a per use basis, so one button using this class may need extra padding (p-5) than another (p-10). You can also use the @layer attribute to achieve the same result.

The apply attribute will take all the utility classes and combine them under awesome-btn.

We can now update our class in Index.fs to use awesome-btn!

10) Components FTW

Before you go off and start using tailwind utilities to build your own UI framework, you may find a much better approach is to instead build components rather than a CSS class.

let awesomeButton =
    Html.button [
        prop.className
            "bg-blue-500
             h-10 w-20
             rounded
             text-gray-50
             hover:bg-blue-300
             hover:text-gray-500
             focus:ring
             focus:ring-offset-2
             focus:ring-blue-300
             focus:ring-opacity-50
             active:bg-blue-700"
        prop.text "Hi"
    ]

This is just a simple function, to create an actual React component check out Feliz!

This is because CSS classes will hide away a lot of the intent that can be seen from tailwinds utilities. Abstracting away the declarative classes that tailwind provides will make it harder to reason about why things are styled a certain way.

Another is that by using @apply to create custom CSS classes you are duplicating the generated CSS, which can be avoided by making components that have styles built in.

11) Customising our tailwind config

This is really where tailwind shines, it's such an extendable tool since all it does is provide utility classes.

So how do you know what can be extended?

In the Client directory run:

npx tailwindcss init tailwind-full.config.js --full

This will not affect our current setup as tailwind looks for tailwind.config.js

We should now have a tailwind-full.config.js file that explicitly has all the tailwind config. This step is like running an npm eject when using a create-react-app, so it is not generally recommended. We are using it here to figure out how we can customise our tailwind.config.js.

Let's add some new colours to our configuration from the tailwind-full.config.js.

I can see a colors key within the theme object. If we head to our tailwind.config.js, within theme.extend we can add some new colours:

module.exports = {
    purge: [],
    darkMode: false, // or 'media' or 'class'
    theme: {
        extend: {
            colors: {
                "cit-blue": "#102035",
                "cit-light-blue": "#40a8b7",
                "cit-green": "#8cbf41",
                "cit-yellow" : "#fec903",
                "cit-red": "#e1053a",
                "cit-orange": "#e97305"
                }
        },
  },
    variants: {
        extend: { backgroundColor: ['active'] },
    },
    plugins: [],
}

But we can make this a bit nicer by nesting the structure:

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
        colors: {
            cit: {
                "DEFAULT": "#102035",
                "light-blue": "#40a8b7",
                "green": "#8cbf41",
                "yellow" : "#fec903",
                "red": "#e1053a",
                "orange": "#e97305"
            }
        }
    },
  },
  variants: {
    extend: { backgroundColor: ['active'] },
  },
  plugins: [],
}

Now we can do something like:

bg-cit-red
text-cit /* This will take the DEFAULT value which is our dark blue */

12) The Purge 🔪

Finally we're ready to Productionise our code. Tailwind generates a lot of css, some of which may never be used. In order to remove the uneeded classes we need to update the purge array in our tailwind.config.js:

  purge: [ "./Index.fs"]

I've only started looking into Tailwind this week and it's already becoming a core technology in my workflow. I've only just scratched the surface and I can't wait to share more! ❤