Skip to content

How to add Tailwind CSS to Blazor website

In how to deploy a Blazor site to Netlify I walked through the process of how to deploy a Blazor Web Assembly site to netlify using Cake and GitHub Actions. We deployed the standard template project warts and all.

In this article we're going to take a look at how we can start customising our project a little and start using Tailwind CSS. I would want to caveat this, with the fact that I am more of a Backend Engineer and as a result I don't really do that much Front End engineering or even design or graphics work on a day to day basis. So I'll freely admit that there are probably a lot better UI frameworks out there and from what I have read on the internet Tailwind does get a lot of bad press and hate among the Front End engineers, but I like it and it helps me with my limited skills and knowledge of front end to create at least presentable front ends.

Create empty Blazor Wasm Site

In how to deploy a Blazor site to Netlify we created a new Blazor wasm project using the default template that comes with Dotnet, which was great for a sample project because it comes with some default features that an average app would need, and for a sample project this is fine. However, when you're starting a brand new project you don't really want to be spending a lot of time fiddling with the sample project to make it your own. This is why dotnet also comes with a Minimal project structure or what is is termed as a Empty blazor project that you can use to start new application.

For this walk-through guide I am going to assume you have already created your Git and GitHub repository as we did in the how to deploy a Blazor site to Netlify , we won't go through the process here again.

dotnet new balzorwasm-empty -o src/front

Initialise Tailwind

You are going to need Node installed to use Tailwind, which is relatively a simple process to do using Homebrew and using Node Version Manager, How to Install Node Version Manager on Ubuntu Linux will guide you through the process, of doing so.

Although Blazor makes it possible to develop single-page applications in C# instead of using JavaScript or Typescript, there are still cases where you may need to call out to JavaScript to accomplish certain tasks or integrations. For instance, Browser based API's or interacting with existing JavaScript libraries etc.

How to use TypeScript with Blazor

C# is a strongly typed language, and what this means is that variables are known at compile time instead of run time, which means C# enforces type safety. ensuring you cannot assign a string value to a variable declared as an integer without an explicit conversion. Such constraints ensure that operations are type-safe, minimising runtime errors due to type mismatches.

Blazor makes it possible to develop single-page applications in C# instead of JavaScript or TypeScript, there are still cases where you need to call JavaScript code to accomplish something, such as calling browser APIs or interacting with existing JavaScript libraries. 

I have to prefer using TypeScript over vanilla Javascript when writing integrating with JavaScript in my Blazor apps, primarily becauseTypeScript is a superset of JavaScript that adds optional static typing to the language. It is designed to help catch mistakes early through a type system and to make JavaScript development more efficient .

I know the Front End community is split over Typescript and it seems many are diverting away from it, but as I have said before I am a primarily Back end Engineer, and quite frankly when it comes to all things Front End Related I often need all the help I can get!

So although it may not be for everyone I prefer to use the TailwindCSS TypeScript integration, and as a result there are a few additional steps to take when configuring your Blazor project to work with TypeScript.

The first step is to reference an additional library in your Blazor Wasm such as the Microsoft.TypeScript.MSBuild , which is a TypeScript MSBuild Task, which helps to Transpile our TypeScript to Javascript when our Blazor Project is built.

We can simply add the library using the dotnet cli as follow or you can use whichever IDE based tool to reference the package to your project.

dotnet add package Microsoft.TypeScript.MSBuild

The next thing we need to do it generate the TypeScript configuration file, commonly known as the tsconfig.json , we can do this using npx as follows in the root of our Front end project,

cd src/front
npx -p typescript tsc --init

This will generate a tsconfig.json file with a lot of comments providing all the options you can choose to implement. I usually simply just replace all the content with the following configuration options.

  "compilerOptions": {
    "target": "es2020",
    "module": "es2020",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "moduleResolution": "bundler"

The next step is generate our Tailwind Configuration file for TypeScript , we can use the npx tailwindcss initialise method by passing the TypeScript switch to it

npx  tailwindcss init --ts

We can open our Tailwind Configuration file and add the following additional line to it, which instruct Tailwind to instruct Tailwind to scan files related to our Blazor project for CSS markup, by adding the content configuration.

import type { Config } from 'tailwindcss'

export default {
  content: [
  theme: {
    extend: {},
  plugins: [
} satisfies Config

For the most basic implementation we are complete here because if we run the following command line we could generate a CSS file for our application

npx tailwindcss --minify -i ./src/front/styles/app.css -o ./src/front/wwwroot/css/app.css --watch --config ./src/front/tailwind.config.ts

However, this will require developers on your team to remember to run this, and from experience this is not always the best. So what we're going to do is add some additional automation by creating an additional MSBuild target that can be executed everytime we start the project and even during the build process.

Add your Development CSS File which we create in a folder styles\app.css and we'll add the following base styles to it

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

    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;}
#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;}


You may notice above that I have also included some of the previous styles in the stylesheet. I just wanted to do this, to provide an illustration that although we are going to use to Tailwind, you are still able to use other CSS rules of even still create your own.

In forthcoming posts, I will highlight and provide further walk through on how you can further optimise using Tailwind CSS in your Blazor Web Assembly apps

Configure MSBuild Target

We are going to create an MSBuild target, which is a fundamental concept in the build process used by MSBuild, Microsoft's build system. Targets group together a sequence of tasks, which are the individual steps that perform the actual work, such as compiling code or copying files.

In our Build target we are going to install the required and any additional NPM Packages our app will use, and also configure a watch or publish events. Learn more about MSBuild Targets

Lets create our target file as follows:

touch tailwind.targets

The intention behind our target, is first to check if Node is installed on the machine currently executing the build script, if Node is not currently present inform the user of the action to take.

If node is present then we need to install the required packages. Once all the required packages are installed then we can run various scripts that we will define in the package.json. We also need to ensure that all these steps are conducted Before we start the build.

     <Target Name="NpmInstall" BeforeTargets="TailwindCSS" Inputs="$(MSBuildThisFileDirectory)package.json" Outputs="$(MSBuildThisFileDirectory)package-lock.json">
        <Message Text=" Install Check for node " Importance="high"></Message>
        <Exec Command="node -v" ContinueOnError="true" StandardOutputImportance="low">
            <Output TaskParameter="ExitCode" PropertyName="error" />
        <Error Condition="'$(error)' != '0'" Text="Node.JS is required to install Tailwind and associated libraries,  Node is not present" />
        <Exec Command="npm install" />
        <Message Text="Npm Install Check completed successfully!" Importance="high"></Message>
    <Target Name="TailwindCSS" AfterTargets="BeforeBuild" Condition="'$(TailwindBuild)' == 'true'">
        <Message Text="TailwindCSS Starting..." Importance="high"></Message>
        <Exec Command="npm run build" Condition="'$(Configuration)' == 'Debug'"/>
        <Exec Command="npm run publish" Condition="'$(Configuration)' == 'Release'"/>
        <Message Text="TailwindCSS Finished !" Importance="high"></Message>

Now that we have a Build Target created we just need to update our .csproj to use, so we open the project file for editing and just add the following line as highlighted below

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

  <Import Project="tailwind.targets" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.14" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.14" PrivateAssets="all" />
    <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.2.2" />

    <Folder Include="wwwroot\css\" />

Add Tailwind plugins

Tailwind have developed a few additional plugins for popular features that don't necessarily belong in the core package, and personally I have found myself needing them most of the time for the types of apps I create so I usually take the time to always add them as a matter of course.

Lets go ahead and add them now, because we are going to need a package.json anyway to write the additional scripts we need. Check out TailwindCSS Plugins for more information on the Tailwind Plugins

In my case I am going to add the following plugins

## Change into your Blazor project folder
cd src/front

npx npm install tailwindcss @tailwindcss/typography @headlessui/tailwindcss @tailwindcss/aspect-ratio @tailwindcss/forms .

# we also want to ensure we install the Types/node for TypeScript
npx npm install @types/node -D .

#  The . at the end is important because that tells npx to create the package.json in the current directory

Add Build Scripts

In our tailwind.targets we alluded to using some additional scripts as part of our build process, we are going to create them now. essentially what these scripts will do is generate a Minimised CSS file to the wwwroot/css/app.min.css

add the following the package.json file that we created in the previous stop

"scripts": {
    "build": "npx tailwindcss --minify  --config ./tailwind.config.ts  -i ./styles/app.css -o ./wwwroot/css/app.min.css",
    "publish": "npx tailwindcss --minify --config ./tailwind.config.ts -i ./styles/app.css -o ./wwwroot/css/app.min.css"

Understanding the wwwroot folder

The wwwroot in Blazor is considered the root folder of your project, although it is not the root in your file explorer. The wwwroot is the folder that where you place all your app's static files including the global css styles, your main HTML file of the project i.e. index.html, and additional JavaScript , assets ( images, icons, static documents etc.
). It is also the place to place JSON files that represent the configuration of your app.

The body of index.html contains, by default, a div with the ID app (this is basically where all the UI that you see in any Blazor app resides), and another div with the ID blazor-error-ui, which is a simple design div that shows up when an unhandled exception occurs


The term Single-Page Application (SPA) may appear to be misleading when you open the app and start navigating from one page to another. However, technically SPAs consist of a single HTML page and Blazor or any other JavaScript framework (Vue, ReactJS, Angular, etc.) dynamically updates the content of that page.

Update index.html to use Minified CSS file

If you haven't deduced so far, our build process is going to create a Minified CSS file derived from the one we created in our styles\app.css so we just need to configure our app to use that. So we'll simply update the index.html in the wwwroot folder to load the app.min.css that will be created as part of build process

<!DOCTYPE html>
<html lang="en">

    <meta charset="utf-8" />
    <base href="/" />
    <link href="css/app.min.css" rel="stylesheet" />

    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    <script src="_framework/blazor.webassembly.js"></script>


Edit Index.razor

We can add the following Tailwind markup to our index.razor file. The code we're going to add is mostly purely to demonstrate that Tailwind CSS is working and that we can use the utility classes it contains to build a nice front end.

@page "/"
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
    Background backdrop, show/hide based on modal state.

    Entering: "ease-out duration-300"
      From: "opacity-0"
      To: "opacity-100"
    Leaving: "ease-in duration-200"
      From: "opacity-100"
      To: "opacity-0"
  <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>

  <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
    <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
        Modal panel, show/hide based on modal state.

        Entering: "ease-out duration-300"
          From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
          To: "opacity-100 translate-y-0 sm:scale-100"
        Leaving: "ease-in duration-200"
          From: "opacity-100 translate-y-0 sm:scale-100"
          To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
      <div class="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
          <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
            <svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
          <div class="mt-3 text-center sm:mt-5">
            <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">Tailwind Configuration Successful</h3>
            <div class="mt-2">
              <p class="text-sm text-gray-500">The Tutorial on how to configure Blazor WASM with tailwind has been successful</p>
        <div class="mt-5 sm:mt-6">
          <a type="button" class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" href="" >Go to Tutorial</a>

Run the application

If we now run the application, by which ever means you choose. In my case it will simply by clicking the run button in Jetbrains Rider, we should be able to browse to the Development Url and see the fruits of our labour.

Commit and deploy the application

In how to deploy your Bazor site to netlify I walked through the process of how to build and deploy your Blazor wasm project using Cake and Github actions, we can use the exact same steps for this project and once complete we view the results of application actually deployed to Netlify for the world to see.


I have to admit there were quite a lot of steps to configure your Blazor wasm project to use Tailwind CSS but in my opinion it is definitely worth it certainly if you're as bad at coming with pleasing designs as I am.

Gary Woodfine
Latest posts by Gary Woodfine (see all)