If you've spent any time in the React ecosystem over the past two years, you've seen shadcn/ui everywhere. And for good reason — it solved something fundamental that every other component library got wrong.
I'm a Blazor developer. And for the longest time, watching shadcn/ui take over the React world felt like watching a party happening through a window.
This is the story of why I built NeoUI, what I learned, and how Blazor developers can now have the same thing.
The shadcn/ui moment
When shadcn/ui launched, it didn't just ship a set of components. It shipped a philosophy.
The insight was simple but radical: you shouldn't be fighting your component library. Most UI libraries are configuration-first — you install them, accept their design system, and spend half your time overriding their opinions. Customising a MUI component to not look like MUI is a rite of passage every React developer has suffered through.
shadcn/ui flipped this. Instead of locking you into a package you can't modify, it gives you the source. Components live in your project. You own them. You change them however you want. There's no node_modules black box to fight.
The other half of the philosophy — composability over configuration — borrowed from Radix UI's headless primitives model. Behaviour (accessibility, keyboard navigation, ARIA) is separated from appearance. You get the hard parts for free, and the visual layer is entirely yours.
The React ecosystem embraced it almost immediately. By early 2024, shadcn/ui had become the de-facto starting point for new React projects. Every design system article, every SaaS starter kit, every "how I built my dashboard" post referenced it.
The Blazor gap
Here's what the Blazor ecosystem looked like during that same period.
The main options were MudBlazor, Radzen, Blazorise, and the Microsoft Fluent UI library. Each of them is genuinely good at what it does. MudBlazor has excellent documentation and a passionate community. Radzen ships a remarkable number of components for a free library. But they all share the same fundamental model:
Install the package. Accept the design system. Style within their constraints.
MudBlazor looks like Material Design because it is Material Design. That's not a criticism — it's a deliberate, coherent choice. But if you're building a SaaS product with its own brand, or a dashboard that needs to look nothing like any other Blazor app, you're immediately in override territory. You end up writing more CSS to fight the library than you would have written starting from scratch.
I kept looking at the React world and thinking: Blazor deserves better than this. C# on the frontend is genuinely a superpower — strongly typed components, the full .NET ecosystem, no context-switching between languages. Why should the UI layer be the weak point?
So I went looking. And I did find a few attempts at bringing shadcn/ui to Blazor. But none of them were what I needed.
Some were early experiments that had gone dormant — a handful of components, last commit months ago, clearly abandoned before they got anywhere near production-ready. Others took an approach that felt fundamentally wrong for the platform: wrapping React components and calling them Blazor. That's not Blazor UI — that's JavaScript with a C# facade bolted on. You lose IntelliSense, type safety, the Razor component model, and any real integration with Blazor's rendering pipeline. It defeats the entire point of using Blazor in the first place.
What I couldn't find was anything that was simultaneously serious, well-architected, and comprehensive. Something built natively on Blazor from the ground up, with a proper two-layer architecture, real accessibility, a maintainable codebase, and enough components to actually ship a production application. Not a proof of concept. Not a wrapper. A real library.
The answer wasn't to build another component library with another design system. The answer was to bring the shadcn/ui philosophy to Blazor — properly, natively, completely.
What NeoUI is (and isn't)
NeoUI is not a port of shadcn/ui. It's a Blazor component library that adopts shadcn/ui's design principles:
- Composability over configuration — components are built to be composed, not configured through a wall of parameters
- You own your UI — primitives give you headless behaviour you can style completely
- Zero lock-in — no mandatory design system, no "MudBlazor aesthetic"
- Pre-built is optional — use the styled layer for speed, or drop to primitives for full control
And critically — it's fully compatible with shadcn/ui and tweakcn themes. If you copy a theme from ui.shadcn.com/themes and paste it into your wwwroot/styles/theme.css, NeoUI uses it immediately. The same theme your React colleagues use. The same customisation tools. The same mental model.
Architecture: the two-layer model
The design that makes this possible is a two-package architecture.
NeoUI.Blazor → Styled components (pre-built CSS, ready to use)
NeoUI.Blazor.Primitives → Headless components (no CSS, full control)
The styled layer is what most developers will use. Install NeoUI.Blazor, add two lines to App.razor, and you have 100+ production-ready components that look great out of the box. No Tailwind setup. No Node.js. No build tooling.
The primitives layer is for teams who need complete control. Every complex component — Dialog, Dropdown, Select, Tooltip, Sheet — has a corresponding headless primitive that handles all the accessibility, keyboard navigation, and ARIA attributes, with zero styling attached. You bring your own CSS.
This is directly analogous to how Radix UI underlies shadcn/ui in the React world.
shadcn/ui vs NeoUI — side by side
The parallel is easiest to see in code. Here's the same Dialog component in both ecosystems.
shadcn/ui (React)
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
export function ConfirmDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Continue</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
NeoUI (Blazor)
<Dialog>
<DialogTrigger AsChild>
<Button Variant="ButtonVariant.Outline">Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose AsChild>
<Button Variant="ButtonVariant.Outline">Cancel</Button>
</DialogClose>
<Button>Continue</Button>
</DialogFooter>
</DialogContent>
</Dialog>
The structural parallel is intentional and precise. If you've used shadcn/ui, NeoUI feels immediately familiar. The same AsChild pattern from Radix UI — allowing a trigger to render as your own element rather than a default button — is fully implemented.
Another example: Select
shadcn/ui (React)
<Select>
<SelectTrigger>
<SelectValue placeholder="Pick a framework" />
</SelectTrigger>
<SelectContent>
<SelectItem value="blazor">Blazor</SelectItem>
<SelectItem value="react">React</SelectItem>
<SelectItem value="vue">Vue</SelectItem>
</SelectContent>
</Select>
NeoUI (Blazor)
<Select>
<SelectTrigger>
<SelectValue Placeholder="Pick a framework" />
</SelectTrigger>
<SelectContent>
<SelectItem Value="blazor">Blazor</SelectItem>
<SelectItem Value="react">React</SelectItem>
<SelectItem Value="vue">Vue</SelectItem>
</SelectContent>
</Select>
Same component tree. Same mental model. The only difference is C# conventions — PascalCase attributes, strongly typed values. For a team that works across React and Blazor, this means near-zero context switching.
Headless primitive: Dialog
When you need full control, drop to the primitive layer:
@using NeoUI.Blazor.Primitives.Dialog
<DialogPrimitive>
<DialogPrimitiveTrigger AsChild>
<button class="my-custom-trigger">Open settings</button>
</DialogPrimitiveTrigger>
<DialogPrimitiveContent class="my-dialog-panel">
<DialogPrimitiveTitle class="my-dialog-title">
Settings
</DialogPrimitiveTitle>
<!-- Your completely custom markup and styles -->
<div class="my-dialog-body">
<p>Everything here is yours. NeoUI handles focus trapping,
Escape key, scroll lock, and ARIA — you handle the rest.</p>
</div>
<DialogPrimitiveClose class="my-close-button">✕</DialogPrimitiveClose>
</DialogPrimitiveContent>
</DialogPrimitive>
No NeoUI styles apply. No CSS variables referenced. The primitive gives you:
- Focus trapping inside the dialog
- Escape key to close
- Scroll lock on body
-
role="dialog",aria-modal,aria-labelledbywired automatically - Keyboard navigation and screen reader support
You provide everything visual.
Theming: drop in any shadcn/ui theme
This is one of NeoUI's most practical advantages. The entire ecosystem of shadcn/ui themes works out of the box.
Go to ui.shadcn.com/themes or tweakcn.com, pick or customise a theme, copy the CSS variables, and paste them into wwwroot/styles/theme.css:
@layer base {
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--radius: 0.5rem;
/* ... */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
/* ... */
}
}
Reference it before NeoUI's CSS in App.razor:
<link rel="stylesheet" href="styles/theme.css" />
<link href="@Assets["_content/NeoUI.Blazor/components.css"]" rel="stylesheet" />
<script src="@Assets["_content/NeoUI.Blazor/js/theme.js"]"></script>
Every NeoUI component picks up your theme variables automatically. Dark mode works by adding .dark to <html> — no JavaScript required, no flicker (theme.js applies the saved preference before Blazor loads, preventing FOUC).
Beyond static themes, NeoUI ships a ThemeService and ThemeSwitcher component for runtime switching across 85 combinations — 5 base colours × 17 primary colours — all without a page reload.
What's inside
After 828 commits, NeoUI currently ships:
100+ styled components covering every UI pattern a real application needs:
- Forms: Button, Input, Select, Combobox, DatePicker, DateRangePicker, ColorPicker, MaskedInput, CurrencyInput, NumericInput, OTP, Rating, RangeSlider, MultiSelect and more
- Layout: Sidebar, Accordion, Tabs, Resizable, Carousel, NavigationMenu, Breadcrumb
- Overlays: Dialog, Sheet, Drawer, AlertDialog, ContextMenu, DropdownMenu, Toast, Popover, Tooltip, HoverCard, Command palette
- Data: DataTable (with sorting, filtering, column pinning, virtualisation, tree rows, server-side), Filter builder, RichTextEditor, MarkdownEditor
- Charts: 12 types including Candlestick, Gauge, Heatmap, Funnel, Radar
- Animation: Declarative Motion system with 20+ presets, scroll-triggered, spring physics
15 headless primitives for every complex interaction pattern
3,200+ icons across three packages: Lucide (1,640), Heroicons (1,288), Feather (286)
Built-in localisation via ILocalizer — works across Server, WebAssembly, and Auto modes
Full .NET 10 support with Auto rendering mode — fast server-side initial load, seamless WebAssembly takeover
Getting started
There are two paths depending on whether you're starting fresh or adding NeoUI to an existing project.
New project — use the template
If you're starting a new Blazor app, the NeoUI project template is by far the fastest path. One command scaffolds a complete production-ready app with sidebar layout, theme switcher, dark mode toggle, Spotlight command palette (Ctrl+K), and a full Tailwind CSS v4 pipeline pre-wired — all supporting .NET 10's Auto, Server, or WebAssembly rendering modes.
dotnet new install NeoUI.Blazor.Templates && dotnet new neoui -n MyApp
cd MyApp && dotnet run --project MyApp
The Tailwind build runs automatically on every dotnet build — Node.js just needs to be on your PATH. No manual configuration.
The template supports all three rendering modes as a first-class option:
dotnet new neoui -n MyApp # Auto (default) — Server + WASM
dotnet new neoui -n MyApp -in Server # Server-side only, single project
dotnet new neoui -n MyApp -in WebAssembly # WebAssembly only
dotnet new neoui -n MyApp --empty # Auto, skip sample pages
What you get out of the box:
| Component | What it does |
|---|---|
Sidebar |
Collapsible sidebar with icon-only collapsed state |
ThemeSwitcher |
Runtime base color selector |
DarkModeToggle |
Light / dark / system mode toggle |
SpotlightCommandPalette |
Ctrl+K command palette |
ReconnectModal |
Custom SignalR reconnect UI (Server + Auto) |
AppLoader |
WASM initialization progress overlay |
Auto and WebAssembly modes scaffold a two-project solution (MyApp + MyApp.Client). Server mode is a single project.
Existing project — manual install
Adding NeoUI to an existing Blazor project takes about five minutes. The primary package is all you need — primitives and all three icon libraries come along as transitive dependencies.
1. Install the package
dotnet add package NeoUI.Blazor --version 3.6.7
2. Add namespace imports to _Imports.razor
@using NeoUI.Blazor
@using NeoUI.Blazor.Services
@using NeoUI.Icons.Lucide @* optional but highly recommended — 1,640 icons *@
Chart components live in their own sub-namespace — add @using NeoUI.Blazor.Charts to any file that uses them.
3. Register services in Program.cs
using NeoUI.Blazor.Extensions;
using NeoUI.Blazor.Primitives.Extensions;
builder.Services.AddNeoUIPrimitives();
builder.Services.AddNeoUIComponents();
For Auto and WebAssembly projects, register in both the server and client Program.cs files.
4. Add CSS references to App.razor
<head>
<!-- Pre-built NeoUI styles — no Tailwind setup required -->
<link rel="stylesheet" href="@Assets["_content/NeoUI.Blazor/components.css"]" />
<!-- Pick one base color and one primary color -->
<link rel="stylesheet" href="@Assets["_content/NeoUI.Blazor/css/themes/base/zinc.css"]" />
<link rel="stylesheet" href="@Assets["_content/NeoUI.Blazor/css/themes/primary/blue.css"]" />
<!-- Prevents flash of unstyled content on page load -->
<script src="@Assets["_content/NeoUI.Blazor/js/theme.js"]"></script>
</head>
The @Assets[...] directive is the .NET 10 way to reference static web assets — it adds a fingerprint hash for automatic cache invalidation on each deployment. To enable the full ThemeSwitcher with all 85 runtime theme combinations, include all base and primary theme files (see the Theming guide).
5. Add portal hosts to MainLayout.razor
Overlay and notification components render outside the component tree via a portal system. All four hosts belong at the end of your layout, after @Body and outside any scrollable or clipping container:
@inherits LayoutComponentBase
<div class="min-h-screen bg-background">
@Body
</div>
<ToastViewport />
<DialogHost />
<ContainerPortalHost />
<OverlayPortalHost />
Each host has a specific role:
-
ToastViewport— renders toast notifications (position configurable) -
DialogHost— required for programmaticDialogServiceusage -
ContainerPortalHost— inline overlays: Popover, Tooltip, DropdownMenu -
OverlayPortalHost— full-screen overlays: Dialog, Sheet, Drawer
6. Start building
<Button>Default</Button>
<Button Variant="ButtonVariant.Outline">Outline</Button>
<Button Variant="ButtonVariant.Secondary">Secondary</Button>
<Dialog>
<DialogTrigger AsChild>
<Button Variant="ButtonVariant.Outline">Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Welcome to NeoUI</DialogTitle>
<DialogDescription>Beautiful Blazor components inspired by shadcn/ui.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose AsChild>
<Button Variant="ButtonVariant.Outline">Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
No Tailwind. No Node.js. No build tools. Just install and build.
Why now
.NET 10's Auto rendering mode changes what's possible with Blazor. The old friction of choosing between Server and WebAssembly — fast load vs rich interactivity — disappears. Auto mode gives you both: server-rendered first load, WebAssembly takeover after download, seamless to the user.
NeoUI is built for this from the ground up. Every component, the portal system, the theme layer, and the project template all support Auto, Server, and WebAssembly equally. The template scaffolds the two-project Auto solution structure correctly by default — something that's surprisingly fiddly to set up manually.
The timing feels right. Blazor has matured. .NET 10 is compelling. The component library ecosystem just needed something that treated Blazor developers as first-class citizens with first-class taste.
Built for agents and AI-assisted development
Here's something most component libraries haven't thought about yet: your AI coding assistant is only as useful as the documentation it can read.
NeoUI ships a complete set of LLM-optimised docs at neoui.io/llms.txt — structured specifically for consumption by Claude, GitHub Copilot, GPT, and other AI coding tools. This isn't an afterthought. It's a structured documentation index covering every part of the library:
Getting Started : https://neoui.io/llms/getting-started.txt
Installation : https://neoui.io/llms/installation.txt
Components : https://neoui.io/llms/components.txt
Full Reference : https://neoui.io/llms/components-full.txt
DataGrid : https://neoui.io/llms/datagrid.txt
Theming : https://neoui.io/llms/theming.txt
Architecture : https://neoui.io/llms/architecture.txt
Primitives : https://neoui.io/llms/primitives.txt
Templates : https://neoui.io/llms/templates.txt
Patterns : https://neoui.io/llms/patterns.txt
Icons : https://neoui.io/llms/icons.txt
Blocks : https://neoui.io/llms/blocks.txt
Changelog : https://neoui.io/llms/changelog.txt
In practice this means when you ask Claude Code or Copilot to "add a server-side DataGrid with sorting and filtering", it generates correct NeoUI code — accurate component names, correct parameter casing, the right service registration — instead of hallucinating APIs that don't exist.
The patterns.txt file alone is worth bookmarking. It covers the real-world scenarios developers actually hit: validated forms, server-side DataGrid, programmatic DialogService, runtime theme switching, SidebarProvider with static rendering, keyboard shortcuts, loading/error/empty state patterns, and the render mode gotchas that trip up most Blazor developers new to Auto mode. Here's a taste — the complete validated login form pattern:
<EditForm Model="@_model" OnValidSubmit="HandleSubmit">
<DataAnnotationsValidator />
<FieldGroup>
<Field>
<FieldLabel For="email">Email</FieldLabel>
<FieldContent>
<Input Id="email" Type="InputType.Email"
@bind-Value="_model.Email"
Placeholder="you@example.com" />
</FieldContent>
<ValidationMessage For="() => _model.Email" />
</Field>
<Field>
<FieldLabel For="password">Password</FieldLabel>
<FieldContent>
<Input Id="password" Type="InputType.Password"
@bind-Value="_model.Password" />
</FieldContent>
<ValidationMessage For="() => _model.Password" />
</Field>
</FieldGroup>
<Button Type="ButtonType.Submit" Class="mt-4">Sign in</Button>
</EditForm>
@code {
private LoginModel _model = new();
private async Task HandleSubmit()
{
// validated, safe to submit
}
private class LoginModel
{
[Required, EmailAddress]
public string Email { get; set; } = "";
[Required, MinLength(8)]
public string Password { get; set; } = "";
}
}
This is what "built for agents" actually means — not a marketing claim, but documented patterns that make AI-assisted development with NeoUI accurate from the first prompt. As agentic coding workflows become standard, the libraries that invested in machine-readable documentation early will have a significant advantage. NeoUI is already there.
What's next
NeoUI is already shipping fast — v3.7.0 (released March 22, 2026) introduced Sidebar Pill Mode, which collapses the sidebar into a floating pill-shaped nav bar on desktop, with five new companion components (SidebarPillNav, SidebarPillNavItem, SidebarPillFade, SidebarPillInset, SidebarPillSpacer). All changes are additive and fully backward-compatible. The roadmap ahead includes:
- Figma kit — design tokens and component specs aligned to NeoUI's design system
- More primitives — expanding the headless layer with additional accessibility primitives
- Blocks — pre-built full-page sections and layout patterns (already in early preview at neoui.io/blocks)
- AI-assisted components — exploring integration points for AI-powered form validation and content generation
The project is open source, MIT licensed, and actively maintained. Contributions, issues, and feedback are all welcome.
Links
- 🌐 Website & docs: neoui.io
- 🎮 Live demo: demos.neoui.io
- 📦 NuGet: NeoUI.Blazor
- 🐙 GitHub: github.com/jimmyps/blazor-shadcn-ui
- 🐦 X: @neoui_io
If you've been waiting for shadcn/ui for Blazor — this is it. Give it a star on GitHub, try it on your next project, and let me know what you think.
Top comments (0)