Tailwind CSS

Introduction

Yes, while bootstrap is the default CSS framework in IHP, you can also use IHP together with Tailwind CSS.

Installing

Adding package to flake.nix

While it’s possible to have nodejs installed via nix, and then have npm install Tailwind CSS, we can skip that part and have nix install the CLI directly.

In the flake.nix, add the tailwindcss package bundled with the most common official plugins.

...
packages = with pkgs; [
    # Native dependencies, e.g. imagemagick
    (nodePackages.tailwindcss.overrideAttrs
        (_: {
            plugins = [
                nodePackages."@tailwindcss/aspect-ratio"
                nodePackages."@tailwindcss/forms"
                nodePackages."@tailwindcss/language-server"
                nodePackages."@tailwindcss/line-clamp"
                nodePackages."@tailwindcss/typography"
            ];
        })
    )
];
...

Rebuild your development environment to fetch the added package:

direnv reload

After that, you should be able to verify that tailwindcss CLI is available in your project directory by executing it from your shell.

tailwindcss

Configuring Tailwind

Create a new directory tailwind. We’re going to place all the CSS files and the tailwind configuration in that directory:

mkdir tailwind

Create the tailwind configuration file at tailwind/tailwind.config.js with the following content:

const plugin = require('tailwindcss/plugin');

module.exports = {
    theme: {
        extend: {
        },
    },
    content: [
        "Web/View/**/*.hs",
    ],
    safelist: [
        // Add custom class names.
        // https://tailwindcss.com/docs/content-configuration#safelisting-classes
    ],
    plugins: [
        require('@tailwindcss/forms'),
    ],
};

We also need a CSS entry point for Tailwind. Create a new file at tailwind/app.css.

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

@layer components {
  .btn {
    @apply px-4 py-2 bg-blue-600 text-white rounded;
  }
}

Integrating with devenv

To start the TailwindCSS build process with the IHP dev server, add the following to your flake.nix:

...
ihp = {
    enable = true;
    projectPath = ./.;
    packages = with pkgs; [
        # Native dependencies, e.g. imagemagick
        (nodePackages.tailwindcss.overrideAttrs
            (_: {
                plugins = [
                    nodePackages."@tailwindcss/aspect-ratio"
                    nodePackages."@tailwindcss/forms"
                    nodePackages."@tailwindcss/language-server"
                    nodePackages."@tailwindcss/line-clamp"
                    nodePackages."@tailwindcss/typography"
                ];
            })
        )
    ];
    haskellPackages = p: with p; [
        # Haskell dependencies go here
        ...
     ];
};

# Add TailwindCSS build command here
devenv.shells.default = {
    processes = {
        tailwind.exec = "tailwindcss -c tailwind/tailwind.config.js -i ./tailwind/app.css -o static/app.css --watch=always";
    };
};
...

Now when you use devenv up to start the IHP dev server, the TailwindCSS build process will be started as well. The --watch=always flag forces tailwindcss to always stay running and watch for any CSS changes in your project and rebuild the static/app.css file as needed.

Adding additional build step for production

For production builds, we also need a new make target:

static/app.css:
	tailwindcss -c tailwind/tailwind.config.js -i ./tailwind/app.css -o static/app.css --minify

Make requires tab characters instead of 4 spaces in the second line. Make sure you’re using a tab character when pasting this into the file

You can now execute it with make -B static/app.css

Updating the .gitignore

As the static/app.css is now generated code, it’s best to put the static/app.css into our .gitignore file.

git rm -f static/app.css # Remove the existing app.css
printf '\nstatic/app.css' >> .gitignore
git add .gitignore

Removing bootstrap

Open Web/View/Layout.hs and remove the following <link> and <script> elements that load bootstrap:

<link rel="stylesheet" href="/vendor/bootstrap.min.css" />
<script src="/vendor/popper-2.11.6.min.js"></script>
<script src="/vendor/bootstrap.min.js"></script>

We don’t need to make any additions for Tailwind here. Just get rid of bootstrap.

Bootstrap is also part of the production CSS build, we need to remove that as well. Open Makefile and remove these lines:

CSS_FILES += ${IHP}/static/vendor/bootstrap.min.css
JS_FILES += ${IHP}/static/vendor/popper-2.11.6.min.js
JS_FILES += ${IHP}/static/vendor/bootstrap.min.js

Switching IHP Styling

Right now, your IHP app will still be rendered with some bootstrap CSS class names. We can switch this to use tailwind classes. Since our configuration uses the JIT mode, it means we would need to copy the tailwind CSSFramework from IHP core files, into our custom theme. Otherwise, any css defined by IHP itself will not be caught by Tailwind before purging and keeping only the used CSS classes. This might seem weird initially, having to copy/paste; however, we think it is the best compromise since it’s very likely you would like to change the default classes and get your site a unique view.

Create a file at Web/View/CustomCSSFramework.hs and copy the imports and the contents of the tailwind function from IHP/View/CSSFramework.hs:

module Web.View.CustomCSSFramework (customTailwind) where

import IHP.View.CSSFramework -- This is the only import not copied from IHP/View/CSSFramework.hs
import IHP.Prelude
import IHP.FlashMessages.Types
import qualified Text.Blaze.Html5 as Blaze
import Text.Blaze.Html.Renderer.Text (renderHtml)
import IHP.HSX.QQ (hsx)
import IHP.HSX.ToHtml ()
import IHP.View.Types
import IHP.View.Classes

import qualified Text.Blaze.Html5 as H
import Text.Blaze.Html5 ((!), (!?))
import qualified Text.Blaze.Html5.Attributes as A
import IHP.ModelSupport
import IHP.Breadcrumb.Types
import IHP.Pagination.Helpers
import IHP.Pagination.Types
import IHP.View.Types (PaginationView(linkPrevious, pagination))


-- Copying the contents of 'tailwind' function
customTailwind :: CSSFramework
customTailwind = def
    { styledFlashMessage
    , styledSubmitButtonClass
    , styledFormGroupClass
    , styledFormFieldHelp
-- ... Keep copying the rest of the function

Now JIT will recognize those classes and not purge them.

Open Config/Config.hs and make these changes:

module Config where

import IHP.Prelude
import IHP.Environment
import IHP.FrameworkConfig
import Web.View.CustomCSSFramework -- ADD THIS IMPORT

config :: ConfigBuilder
config = do
    -- See https://ihp.digitallyinduced.com/Guide/config.html
    -- for what you can do here
    option customTailwind -- ADD THIS OPTION
    pure ()

Building for production

Because we defined the static/app.css make task above, the standard process of building IHP CSS applies here as usual:

make static/prod.css

Make will automatically detect that static/app.css is missing and will run make static/app.css to produce that missing file. This will then trigger the tailwind production build.

This means you don’t need to make any changes to your existing deployment process.