Building ASP.NET apps with Tailwind CSS
Date: 2024-01-05 | create | tech | dotnet | tailwind | fsharp | docker | containers |
I like Tailwind for styling my frontends. I find the atomic styles to be easy to compose and I think composability is a great attribute for building Simple Scalable Systems (3S).
I also mostly build my fullstack projects with F# on ASP.NET.
The problem is that Tailwind is really JS / NPM native. So combining Tailwind with other technologies is not as straightforward as you might hope. Yes you could always just get it via CDN but it's usually a large payload that requires a JS load so not recommended for production.
In this post we'll explore another option for using Tailwind with your ASP.NET app that's production-ready.
Q: How can we combine Tailwind with an ASP.NET app?
Answer
In this post we'll be exploring an example ASP.NET app that builds and serves an optimized Tailwind bundle. We'll be using Docker containers to handle builds and orchestration and output a container ready to be run in production anywhere.
Essentially our approach will be:
- ASP.NET app - our app we want to use Tailwind with
- Tailwind CLI - a minimal npm project for building Tailwind
- Docker container - To configure, build, and optimize our Tailwind output, build our ASP.NET app, then place the Tailwind output in our ASP.NET app's static folder
To demonstrate this and prove it works, I'll be using an example app I built. This app is very similar to how I'm using Tailwind / ASP.NET in production and is how we bundle Tailwind / ASP.NET in CloudSeed - my F# project boilerplate.
This example app is a fullstack monolith serving server-side rendered HTML. It is a very simple markdown blog displaying post lists and content on the frontend with Tailwind styles.
Yes this is F# but it uses standard options / services from ASP.NET so will work for C# as well (they both target dotnet / ASP.NET).
- Backend - F# / Giraffe (ASP.NET)
- Frontend - Server-side rendered HTML using string templates
- Data - Entity Framework ORM and DBUp for Migrations
- Hosting - Docker for containerization, Docker Compose for orchestration
Obviously this is overkill to show you Tailwind + ASP.NET bundled but I like to build realish apps to prove this stuff works.
In this post we will be focusing mostly on:
- Setting up ASP.NET to serve static files
- Setting up Tailwind CLI
- Bundling Tailwind + ASP.NET with Docker
We may touch on other parts but it's mostly out of scope. I've linked relevant articles if you'd like to learn more and the full project source is available in the HAMY LABS Example Project Repo for HAMINIONs subscribers.
Alright, now let's build this thing.
Serving static files with ASP.NET
The first thing we need to do is set up our ASP.NET app to be able to serve static files. Here we'll be using that to serve our static Tailwind css output in a known location so we can pull it into our frontend.
When we build our ASP.NET app in Docker, we'll be building this like a normal app. It will simply assume that the Tailwind output is in the correct place at build time which we'll orchestrate with Docker. This is one of the benefits of containerization - we can explicitly declare and orchestrate environmental dependencies without needing to fight the native build tools to do stuff like this (cough cough msbuild).
Here's the folder structure for my ASP.NET App:
- Source - Contains my app source code
- wwwroot - Where the static files will go (this is the ASP.NET default location but you could configure it elsewhere if you want)
- We'll be using a folder
wwwroot/css
to hold our css files. This will be available from our app at url/css/FILE
- We'll be using a folder
- Tailwind - An empty folder for now which will contain the project needed to run Tailwind CLI
The important thing to call out here is that we're using the IApplicationBuilder.UseStaticFiles
option to setup wwwroot
as a static file host.
Yes this is in F# but the same options will apply for C# on ASP.NET
app
.UseStaticFiles()
.UseRouting()
.UseCors(
Action<_>
(fun (b: Infrastructure.CorsPolicyBuilder) ->
// Put real allowed origins in here
b.AllowAnyHeader() |> ignore
b.AllowAnyMethod() |> ignore
b.AllowAnyOrigin() |> ignore)
)
.UseEndpoints(
fun e ->
e.MapGiraffeEndpoints(endpointsList)
)
|> ignore
We're going to assume that the orchestrated Tailwind file is located at wwwroot/css/tailwind.css
and thus that we can access it via url at /css/tailwind.css
. This means we can include it in our html like this:
<!DOCTYPE html">
<html lang="en" data-theme="light>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/tailwind.css">
{ head }
</head>
<body>
{ body }
</body>
<footer>
{ footer }
</footer>
</html>
Building Tailwind for ASP.NET
Now that we have a folder in our ASP.NET app that will serve static files, we just gotta get Tailwind built and in that folder.
For the build part we're going to be using Tailwind CLI which is the recommended way to build Tailwind outside of the JS ecosystem. Again you could always go buildless and use the CDN but that will leave you with a very large payload filled with a bunch of CSS you don't actually use. By using Tailwind CLI we get all the CSS culling / optimization that allows it to only output the CSS you actually use and ends with a payload that's actually reasonable for production.
To setup Tailwind CLI, we're basically following the official Tailwind CLI setup docs.
- Navigate to the empty Tailwind folder inside our ASP.NET project
- Use the npx command to generate a base Tailwind CLI project
After doing this we should have a baseline version of Tailwind ready for building via npx command. This means that we'll be able to build Tailwind as normal (with configuration, custom css, plugins, etc) and output it to a specific file we can use in our ASP.NET app.
But there's still a few things we need to do to get this working.
Create a base css file
The way Tailwind works is it:
- Looks at the classes you want to build (based on a base css file)
- Looks at the code that will be using the classes to see what's used / unused
- Outputs a new css file containing only the classes you've used
Here we just create a basic CSS file to basically tell Tailwind we want it to include all the classes we use:
/Tailwind/Styles/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Configure Tailwind with tailwind.config.js
We can use tailwind.config.js
to configure our Tailwind build. This is useful for things like theming, plugins, etc. Those are optional but what's necessary is to update the content
field to target the types of files we will be writing Tailwind CSS classes in.
This is important. If you get this wrong, Tailwind will act funky:
- Include too little files (i.e. leave out files where you wrote Tailwind) - Tailwind can't know that you used certain classes and therefore won't output those classes and thus those classes will not work and make your frontend look broken
- Include too many files (i.e. include everything including build directories) - Tailwind will take a long time to run cause it's parsing a ton of unnecessary files and will feel like more of a pain than a utility
In my app, I'm building frontend with HTML string templates in F# so all of my markup and css classes will be declared in .fs
files so that's all I need. But I expect a lot of people reading this will be building with C# and / or HTML templates so here we're targeting:
- F# files -
.fs
- HTML files -
.html
- CSHTML files -
.cshtml
If you are writing markup / declaring Tailwind css classes in other files you will need to add them.
Okay - here's my tailwind.config.js
- it says look for all files in the ./Source
directory that end with .fs
, .html
, or .cshtml
/** @type {import('tailwindcss').Config} */
module.exports = {
// hamy: all css in .fs
content: ["./Source/**/*.{fs,html,cshtml}"],
theme: {
extend: {},
},
// hamy: these plugins are just nice-to-haves for my markdown blog
plugins: [
require('@tailwindcss/typography'),
require("daisyui")
],
}
Combining Tailwind and ASP.NET with Docker
Okay so now we've got:
- Basic ASP.NET app setup with the ability to serve static files from
wwwroot/css
- Simple Tailwind project setup to include all Tailwind classes we use in files ending with
.fs
,.html
, and.cshtml
Now we're ready to orchestrate these together with Docker. This docker file will:
Build Tailwind
- Create a layer with Node
- Copy the Tailwind project into it
npm install
so it has its dependencies ready- Copy the App Source code (
./Source
) into the container - Run
npx
on our base css and output it at./out/tailwind.css
Build ASP.NET
- Create a layer with dotnet sdk
- Copy the fsproj in and dotnet restore to get dependencies
- Copy the rest of the code in and publish it
Orchestrate ASP.NET / Tailwind
- Create a layer with dotnet aspnet
- Copy ASP.NET build in here
- Copy the
./out/tailwind.css
file we made into the ASP.NET static folder at./wwwroot/css/tailwind.css
- Run the ASP.NET app
Dockerfile
# Set up Tailwind
FROM node:20-slim as styles
COPY ./Tailwind ./
RUN npm install
COPY ./Source ./Source
RUN npx tailwindcss -i ./Styles/tailwind.css -o ./out/tailwind.css
# Build Dotnet App
FROM mcr.microsoft.com/dotnet/sdk:7.0-bullseye-slim AS build
# EXPOSE 8080
EXPOSE 80
# ENV ASPNETCORE_URLS=http://+:8080
WORKDIR /source
# copy csproj and restore as distinct layers
COPY ./*.fsproj ./
RUN dotnet restore
# copy and publish app and libraries
COPY . .
RUN dotnet publish -c release -o /app
# --no-restore
# Copy build / styles into final location
FROM mcr.microsoft.com/dotnet/aspnet:7.0
WORKDIR /app
COPY --from=build /app .
COPY --from=styles ./out/tailwind.css ./wwwroot/css/tailwind.css
ENTRYPOINT ["dotnet", "App.dll"]
Next
That's it - you should now be able to build an ASP.NET app using Tailwind for styling all orchestrated via Docker containers.
If you liked this post, you might also like:
Want more like this?
The best / easiest way to support my work is by subscribing for future updates and sharing with your network.