Add framework guide for .NET
This PR introduces documentation for setting up Tailwind CSS with .NET projects using the standalone CLI.
This is similar to #1914, but for v4.
The latest updates on your projects. Learn more about Vercel for GitHub.
| Project | Deployment | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| tailwindcss-com | Preview | Comment | Aug 31, 2025 7:31pm |
@marshalhayes Just tried this works well but how do you get tailwind to rebuild the css when using Blazor hot reload when classes in the components change? The build step seems one and done no matter how I tweak it
That is a great question, @cdock1029.
To be honest, I didn't put much thought into this at the time. After thinking about how this could work for the last few days, I'm introducing a few changes which should get us close to an ideal solution.
Download path: The current solution downloads the Tailwind CLI inside $(ProjectDir)/bin. This is somewhat of a bad practice. Multiple projects would each have their own version downloaded, even if they use the same version.
I've modified the download to be placed in one of two places:
- On Linux & MacOS,
$XDG_CACHE_HOMEor$HOME/.cache - For Windows,
%LocalAppData%
Projects that reference the same version would then share the same executable.
The download directory can also be overridden if needed. For example:
<PropertyGroup>
<TailwindDownloadPath>/tmp/tailwind</TailwindDownloadPath>
</PropertyGroup>
Configurable input & output stylesheets: References to the stylesheets were somewhat buried in the project file. I've restructured the csproj to make these more obvious.
<PropertyGroup>
<InputStyleSheetPath>Styles/main.css</InputStyleSheetPath>
<OutputStyleSheetPath>wwwroot/main.build.css</OutputStyleSheetPath>
</PropertyGroup>
Separating Tailwind from the csproj: When I was digging through the MSBuild docs, I found that a csproj can import a targets file.
I've moved all of the Tailwind MSBuild functionality to a separate file (Tailwind.targets), which the csproj imports.
<!-- myapp.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<Import Project="Tailwind.targets" />
</Project>
<!-- Tailwind.targets -->
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Omitted for brevity -->
</Project>
This greatly simplifies the csproj file, and allows users to share the targets across multiple projects.
And lastly, to the question at hand:
Hot reload: This is a weird one. I cannot find a way to run Tailwind's watch functionality automatically when dotnet watch is used. In theory, it should be feasible as there are environment variables available which indicate when dotnet watch is running.
For example, this would only run when inside dotnet watch:
<Exec Command="echo 'Inside dotnet watch'" Condition="$(DOTNET_WATCH) == '1'" />
So, you could do this:
<Target Name="Tailwind" DependsOnTargets="DownloadTailwind" BeforeTargets="Build">
<PropertyGroup>
<TailwindBuildCommand>$(TailwindCliPath) -i $(InputStyleSheetPath) -o $(OutputStyleSheetPath)</TailwindBuildCommand>
</PropertyGroup>
<Exec Command="$(TailwindBuildCommand) --watch" Condition="$(DOTNET_WATCH) == '1'"/>
<Exec Command="$(TailwindBuildCommand)" Condition="$(DOTNET_WATCH) != '1' And '$(Configuration)' == 'Debug'"/>
<Exec Command="$(TailwindBuildCommand) --minify" Condition="$(DOTNET_WATCH) != '1' And '$(Configuration)' == 'Release'"/>
</Target>
...except that the Exec task in MSBuild exits when the command exits (i.e. it would never exit here).
It is not clear to me how to safely run Tailwind's watch functionality via MSBuild. If it's executed as part of dotnet watch, it needs to be terminated when dotnet watch is terminated, as well as not block MSBuild. I'm not sure how to do that.
For now, I'm adding this to the framework guide:
<!-- When building the project, run the Tailwind CLI -->
<!-- This target can also be executed manually. For example, with dotnet watch: `dotnet watch msbuild /t:Tailwind` -->
<!-- In order to use hot reload, run both `dotnet watch run` and `dotnet watch msbuild /t:Tailwind` -->
<Target Name="Tailwind" DependsOnTargets="DownloadTailwind" BeforeTargets="Build">
<!-- Omitted for brevity -->
</Target>
This will allow us to at least not have to find the Tailwind CLI ourselves, but instead let MSBuild do it. The only downside is you have to run both dotnet watch run and dotnet watch msbuild /t:Tailwind together.
I thought of a few more improvements I can make to this. I'll send another update later tonight.
At this point, I would love to hear feedback on this guide. Is there anything missing that we should add?
@philipp-spiess
I would like to point out that I've tested this with both v3 & v4, and it works well.
This solution also supports all the standalone CLI parameters:
-
--input:TailwindInputStyleSheetPath -
--output:TailwindOutputStyleSheetPath -
--optimize:TailwindOptimizeOutputStyleSheet -
--minify:TailwindMinifyOutputStyleSheet -
--watchis supported in a roundabout way viadotnet watch:dotnet watch msbuild /t:Tailwind
Here's what this solution supports:
-
Tailwind version: When a user imports the
Tailwind.targetsfile in theircsproj, they are required to specify a property calledTailwindVersion. This can either belatestor the name of a release (e.g. v4.0.14). -
Download path: This solution provides reasonable default download paths, and allows for a user to override if needed by setting
TailwindDownloadPath. -
Input & output stylesheet: Similar to the above, a user must specify both
TailwindInputStyleSheetPathandTailwindOutputStyleSheetPath. These values correspond to the CLI arguments--inputand--output. -
Minifying the output stylesheet: A user could optionally set
TailwindMinifyOutputStyleSheettotrueto minify the output stylesheet. By default, this solution minifies the output stylesheet when$(Configuration) == 'Release'. This is a .NET thing. -
Optimizing the output stylesheet: A user could optionally set
TailwindOptimizeOutputStyleSheettotrueto optimize the output stylesheet. This corresponds to the--optimizeCLI flag. -
Download the CLI from a custom URL: A user could optionally set
TailwindDownloadUrl. If set, this solution will use that URL instead of GitHub when downloading the CLI. This is likely never going to be set by anyone, but nevertheless I supported it just in case.
There was a bug found on Windows regarding the download path.
I'll introduce a fix tomorrow.
I am also experimenting with making this a NuGet package: https://github.com/marshalhayes/Tailwind.Standalone
Here is a link to the initial version.
This approach comes with a few benefits in terms of releasing future versions, but with more maintenance cost. I'd be interested in hearing thoughts on which approach is preferred.
Should I add tabs to the .NET framework guide, one for the copy-pasted Tailwind.targets file & another for the NuGet version? Would the Tailwind team be interested in taking ownership of this NuGet package?
Hi @marshalhayes you did a great job here!
Please guys have a look in my tailwind-dotnet repo, I'd cover all the MsBuild part as well Hot Reload support. Pls feel free to provide any suggestions. Maybe we could combine our ideas in an Unified Tailwind solution
Here's the out-of-box features
- No external requirements, like NodeJs or Postcss
- Integrated hot-reload, it works with
dotnet watchas well most common IDEs(Visual Studio and Rider) - Minified output on publish
- .NET 9+ static asset support
Would the Tailwind team be interested in taking ownership of this NuGet package
I'm also able for that, would be awesome to turn it as official package
cc @cdock1029
Thanks for sharing.
The purpose of this framework guide is to do exactly that: define a standard way of using Tailwind with .NET.
What are your thoughts on the framework guide?
Hi @marshalhayes
What are your thoughts on the framework guide?
Seems that you've been duplicating something that I already did. Please have a look into my Tailwind Integration, since I already built a completely out-of-box integration. I suggest to use it as standard solution.
It already includes all MsBuild and Hot Reload features just by doing dotnet add package Tailwind.Hosting.
Off course I'm totally able to cherry-pick your sources for co-working π
Well, to be clear, I didn't duplicate anything. I didn't know this even existed.
@kallebysantos, if you want to propose a different solution, I'd recommend opening a separate PR.
I like that your solution supports dotnet watch. That looks to be the only thing missing in this solution.
I'd be interested in the Tailwind team's feedback here. What are the thoughts on the NuGet package? Should it be community owned, or part of Tailwind Labs?
As far as the framework guide goes, would recommending installing Tailwind via a NuGet package be more preferred over the Tailwind.targets file, or some combination of both?
Hi @marshalhayes
Well, to be clear, I didn't duplicate anything. I didn't know this even existed.
Maybe I explain myself wrong, I know that π I think that what I was trying to say is that your PR shares lot of common parts with mine implementation and I'm inviting you to co-working on it.
I didn't know this even existed.
My fault - It was supposed to me announce it like 1y ago, but I never feel confident to it until last week when I decide to post it on reddit. Then I saw that you share the same challenges that I had
No worries. I totally get it.
There are certainly quite a few .NET implementations that are essentially doing the same thing. Here are some of the most popular ones (according to NuGet downloads):
| Package | Downloads | Repository |
|---|---|---|
| Tailwind.Extensions.AspNetCore | 176.6K | GitHub |
| mvdmio.Tailwind.NET | 37.3K | GitHub |
| Tailwind.MSBuild | 30.7K | GitHub |
| Blazorise.Tailwind | 29.7K | GitHub |
I'm not attached to one particular solution over another, but I think we agree having so many choices makes it difficult to know which is the right one.
Let's let the Tailwind team weigh in here.
Hey @kallebysantos @marshalhayes β thanks a lot for opening this PR (and sorry for the delay in getting back to you!). We really appreciate the effort here.
The challenge for us is that nobody on the Tailwind Labs team feels deeply comfortable with .NET, so we don't have the confidence to say which approach is truly the "right" one for the broader .NET ecosystem. Since anything in the official docs carries a lot of weight, we want to be extra careful not to recommend something that might later turn out to be confusing, overly complex, or limiting.
What would help the most is if the .NET community could come to a consensus on a recommended setup β ideally with input from a recognized .NET maintainer or expert. Once there's a clear best practice that the community agrees on, we'd be much more comfortable documenting and promoting it.
Until then, I think it's safest for us not to βpick a sideβ in the docs. I hope that makes sense, and I really appreciate your understanding (and contributions!) here π