Form and function are essential elements of a successful application, each playing a role in delivering our end users’ experience. The mastery of both is challenging in an exploding web landscape. For ASP.NET Core developers, web design might not rank high at the top of our skillset. In the years since the web has become what we know it today, many of us have adopted CSS libraries and toolkits to accelerate our design productivity. The most popular for ASP.NET developers being Bootstrap, as it ships with the default application templates.

This post will see how to integrate an up-and-coming utility-first CSS library called * *Tailwind CSS** and how to start utilizing it in our ASP.NET Core applications.

* *TL;DR: Folks can get a working sample at GitHub to see how this solution works. **

What Is Tailwind?

The authors of Tailwind CSS first released Tailwind in November of 2017, to bring a utility-first framework to web developers. Tailwind’s approach is different than other CSS libraries as it doesn’t focus on building components but on building composable helper CSS classes. Let’s look at an example of a notification card.

<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
  <div class="flex-shrink-0">
    <img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">
  </div>
  <div>
    <div class="text-xl font-medium text-black">ChitChat</div>
    <p class="text-gray-500">You have a new message!</p>
  </div>
</div>

The class attribute contains utility classes designed to set the background color, margins, padding, and font colors. In other frameworks, you might see component classes like card or alert.

The authors of Tailwind admit that someone’s initial reaction to this approach might be to recoil in horror.

Now I know what you’re thinking, “this is an atrocity, what a horrible mess!” and you’re right, it’s kind of ugly. –Tailwind

They ask you to give it a chance and explain some of the advantages of this approach.

You aren’t wasting energy inventing class names. Let’s face it; naming is hard, right? Your CSS stops growing or grows at a much slower rate. We can build new visual components from existing utility classes. Safer changes. Smaller utilities mean a better understanding of the cascade effects and what that means for the overall design.

For developers who want to stick to traditional UI element componentization, Tailwind CSS also offers the @apply processor, which we can use to combine multiple utility classes into one logical concept.

<!-- Using utilities -->
<button class="py-2 px-4 font-semibold rounded-lg shadow-md text-white bg-green-500 hover:bg-green-700">
  Click me
</button>

<!-- Extracting classes using @apply -->
<button class="btn btn-green">
  Button
</button>

<style>
  .btn {
    @apply py-2 px-4 font-semibold rounded-lg shadow-md;
  }
  .btn-green {
    @apply text-white bg-green-500 hover:bg-green-700;
  }
</style>

Tailwind offers multiple approaches, and ultimately a design strategy is critical for any product, and design systems are vital for project velocity and prototyping larger applications. The @apply processor can help solidify common UI elements in a design system without adding additional overhead to HTML payloads.

One of my favorite design authors, Brad Frost, has a great post on the concept of * *Atomic Design**. The Atomic Design methodology recommends starting from the smallest design elements and then incrementally building a system to assemble complete designs by picking pieces.

We’ve barely scratched the surface of Tailwind, and I recommend developers read the documentation to understand the philosophy of Tailwind.

Let’s see how we can start using Tailwind CSS with an ASP.NET Core application.

Dependencies

To get started, you’ll need both the latest .NET SDK and the latest version of NodeJs and NPM. For this post, I used the .NET 5 SDK, Node v15.0.0, and NPM 6.14.9.

Start With An ASP.NET Core Web Project

The first step is to start with an ASP.NET Core web application that has Razor views. The project can either be a Razor Pages application or a traditional ASP.NET MVC application, or a combination of the two.

Utilizing the .NET CLI, we can create either application.

dotnet new webapp
dotnet new mvc

Any one of the commands above will create an ASP.NET Core web application. Let’s move onto the next step, installing Tailwind.

Install Tailwind

We’ll need to initialize our ASP.NET Core project with a package.json for NPM dependencies from the ASP.NET Core project directory. Run the following command.

npm init -y

The command should create a new package.json at the root of your project.

Next, we’ll install Tailwind and its dependencies.

npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

Our package.json file should look something like this. Note the devDependencies section.

{
  "name": "Tailwind",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {},
  "devDependencies": {
    "@tailwindcss/postcss7-compat": "^2.0.2",
    "autoprefixer": "^9.8.6",
    "postcss": "^7.0.35",
    "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Finally, we need to run the Tailwind initialization command.

npx tailwind init -p

When the command is complete, we should have two new files at the root of our web project: postcss.config.js and tailwind.config.js.

Add Build Scripts

The next step is wiring up the Tailwind processing to our ASP.NET Core build. The first step is to modify our package.json to process our input CSS and produce an artifact. Let’s add a new script of css:build. Don’t worry if these files don’t exist yet; we’ll add them in the next section.

{
  "name": "Tailwind",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {},
  "devDependencies": {
    "@tailwindcss/postcss7-compat": "^2.0.2",
    "autoprefixer": "^9.8.6",
    "postcss": "^7.0.35",
    "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2"
  },
  "scripts": {
    "css:build": "npx tailwind build ./wwwroot/css/site.css -o ./wwwroot/css/output.css"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

We also want to modify our ASP.NET Core project’s .csprojto to run this new NPM command.

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <UpToDateCheckBuilt Include="wwwroot/css/site.css" Set="Css" />
        <UpToDateCheckBuilt Include="postcss.config.js" Set="Css" />
        <UpToDateCheckBuilt Include="tailwind.config.js" Set="Css" />
    </ItemGroup>

    <Target Name="Tailwind" BeforeTargets="Build">
        <Exec Command="npm run css:build"/>
    </Target>

</Project>

It’s essential to use UpToDateCheckBuilt to make sure that we rebuild the entire project any time those files change. Without the ItemGroup, we would have to trigger a rebuild manually.

You may notice this approach is very similar to my Vue Js post. I worked hard on that post, and it is an excellent approach if you’re interested in adopting Vue as your frontend framework.

Add CSS Files

Under the wwwroot folder, we can delete any lingering Bootstrap files. The files will be in the directories of ./wwwroot/css and ./wwwroot/lib/bootstrap.

We can now create a new site.css file with the following contents.

/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;

This file is where we can put any custom CSS components utilizing the @apply processor. In general, we can leave the file as-is.

Building the project now should produce an output.css file under ./wwwroot/css. Folks comfortable with Tailwind can likely stop here, but let’s look at how we can modify our views to use Tailwind and prune our CSS using the tailwind.config.js file settings.

Modify Layout and Razor Views

In my project, I decided to use Razor Pages. The _Layout.cshtml will be the same for ASP.NET MVC and Razor Page applications. I’ve decided to use a [Minimal Blog]https://github.com/tailwindtoolbox/Minimal-Blog() sample layout from Tailwind Toolbox.

As a quick start, here is the modified Razor. **Note the reference to our output.css file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>@ViewData["Title"] - Tailwind</title>
    <meta name="author" content="name"/>
    <meta name="description" content="description here"/>
    <meta name="keywords" content="keywords,here"/>
    <link rel="stylesheet" href="~/css/output.css"/>
</head>

<body class="bg-gray-100 font-sans leading-normal tracking-normal">

<nav id="header" class="fixed w-full z-10 top-0">

    <div id="progress" class="h-1 z-20 top-0" style="background:linear-gradient(to right, #4dc0b5 var(--scroll), transparent 0);"></div>

    <div class="w-full md:max-w-4xl mx-auto flex flex-wrap items-center justify-between mt-0 py-3">

        <div class="pl-4">
            <a class="text-gray-900 text-base no-underline hover:no-underline font-extrabold text-xl" href="#">
                Minimal Blog
            </a>
        </div>

        <div class="block lg:hidden pr-4">
            <button id="nav-toggle" class="flex items-center px-3 py-2 border rounded text-gray-500 border-gray-600 hover:text-gray-900 hover:border-green-500 appearance-none focus:outline-none">
                <svg class="fill-current h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
                    <title>Menu</title>
                    <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
                </svg>
            </button>
        </div>

        <div class="w-full flex-grow lg:flex lg:items-center lg:w-auto hidden lg:block mt-2 lg:mt-0 bg-gray-100 md:bg-transparent z-20" id="nav-content">
            <ul class="list-reset lg:flex justify-end flex-1 items-center">
                <li class="mr-3">
                    <a class="inline-block py-2 px-4 text-gray-900 font-bold no-underline" href="#">Active</a>
                </li>
                <li class="mr-3">
                    <a class="inline-block text-gray-600 no-underline hover:text-gray-900 hover:text-underline py-2 px-4" asp-area="" asp-page="Index">
                        Home
                    </a>
                </li>
                <li class="mr-3">
                    <a class="inline-block text-gray-600 no-underline hover:text-gray-900 hover:text-underline py-2 px-4" asp-area="" asp-page="Privacy">
                        Privacy
                    </a>
                </li>
            </ul>
        </div>
    </div>
</nav>

<!--Container-->
<div class="container w-full md:max-w-3xl mx-auto pt-20">
    @RenderBody()
</div>
<!--/container-->

<footer class="bg-white border-t border-gray-400 shadow">
    <div class="container max-w-4xl mx-auto flex py-8">

        <div class="w-full mx-auto flex flex-wrap">
            <div class="flex w-full md:w-1/2 ">
                <div class="px-8">
                    <h3 class="font-bold text-gray-900">About</h3>
                    <p class="py-4 text-gray-600 text-sm">
                        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas vel mi ut felis tempus commodo nec id erat. Suspendisse consectetur dapibus velit ut lacinia.
                    </p>
                </div>
            </div>

            <div class="flex w-full md:w-1/2">
                <div class="px-8">
                    <h3 class="font-bold text-gray-900">Social</h3>
                    <ul class="list-reset items-center text-sm pt-3">
                        <li>
                            <a class="inline-block text-gray-600 no-underline hover:text-gray-900 hover:text-underline py-1" href="#">Add social link</a>
                        </li>
                        <li>
                            <a class="inline-block text-gray-600 no-underline hover:text-gray-900 hover:text-underline py-1" href="#">Add social link</a>
                        </li>
                        <li>
                            <a class="inline-block text-gray-600 no-underline hover:text-gray-900 hover:text-underline py-1" href="#">Add social link</a>
                        </li>
                    </ul>
                </div>
            </div>
        </div>


    </div>
</footer>

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

@await RenderSectionAsync("Scripts", required: false)

<script>
      /* Progress bar */
      //Source: https://alligator.io/js/progress-bar-javascript-css-variables/
      var h = document.documentElement,
         b = document.body,
         st = 'scrollTop',
         sh = 'scrollHeight',
         progress = document.querySelector('#progress'),
         scroll;
      var scrollpos = window.scrollY;
      var header = document.getElementById("header");
      var navcontent = document.getElementById("nav-content");

      document.addEventListener('scroll', function() {

         /*Refresh scroll % width*/
         scroll = (h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight) * 100;
         progress.style.setProperty('--scroll', scroll + '%');

         /*Apply classes for slide in bar*/
         scrollpos = window.scrollY;

         if (scrollpos > 10) {
            header.classList.add("bg-white");
            header.classList.add("shadow");
            navcontent.classList.remove("bg-gray-100");
            navcontent.classList.add("bg-white");
         } else {
            header.classList.remove("bg-white");
            header.classList.remove("shadow");
            navcontent.classList.remove("bg-white");
            navcontent.classList.add("bg-gray-100");

         }

      });


      //Javascript to toggle the menu
      document.getElementById('nav-toggle').onclick = function() {
         document.getElementById("nav-content").classList.toggle("hidden");
      }
   </script>

</body>

</html>

We can also modify Index.cshtml Razor to use Tailwind classes. I’ve copied the content from the Tailwind Toolbox GitHub repository. Running our application now shows our newly styled Tailwind UI.

tailwind powered asp.net core web app

Prune Unused CSS Settings

You may have noticed that the Tailwind build process is generating a 3 Megabyte file in the build output.

0>   @tailwindcss/postcss7-compat 2.0.2
0>  
0>   🚀 Building: wwwroot/css/site.css
0>  
0>   ✅ Finished in 2.55 s
0>   📦 Size: 3.74MB
0>   💾 Saved to wwwroot/css/output.css

Don’t worry about that file size because Tailwind comes with a pruning mechanism that we can set to remove unused CSS utility classes. Let’s change our tailwind.config.js file to scan our Razor views and remove any superfluous CSS from our final output file.

module.exports = {
  purge: {
    enabled: true,
    content: [ 
        './Pages/**/*.cshtml',
        './Views/**/*.cshtml'
    ]
  },
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

When we rebuild our project, we now see a huge file size saving, with the final output being only 19.11KB.

0>   @tailwindcss/postcss7-compat 2.0.2
0>  
0>   🚀 Building: wwwroot/css/site.css
0>  
0>   ✅ Finished in 2.51 s
0>   📦 Size: 19.11KB
0>   💾 Saved to wwwroot/css/output.css

That’s a remarkable feature of the Tailwind build pipeline, and we just fiddled with a few settings.

Conclusion

Design is critical to many web applications, and as ASP.NET developers, we have a lot of options at our disposal. Luckily, with ASP.NET Core and the latest .NET SDKs, we have mechanisms to integrate other valuable tools into our projects easily.

For folks who want a complete running ASP.NET Core project, check out a working sample at this GitHub repository.

Tailwind seems like a promising way to build user interfaces and design systems. I have to admit I’m not an expert, but I hope to learn more in the upcoming months as I dive deeper into the subject.

If you are currently using or thinking about using Tailwind, please leave a comment below. I’d love to hear your thoughts.