A production-ready Blazor component library that replicates shadcn/ui one‑to‑one. Thin component wrappers, Tailwind CSS styling, CSS‑variable theming, and minimal JavaScript interop — across all Blazor render modes (Server, WebAssembly, Auto, Static SSR).
🔗 Live demo: blazorcn.evisoft.com
- ~200 components across 55 groups — full shadcn/ui parity
- 1,702 Lucide icons as components, plus a by‑name
LucideIconCndispatcher - CVA +
Cn.Merge()— C# ports ofclass-variance-authorityandcn()(tailwind‑merge) - Exact shadcn theming — CSS variables,
.darkclass dark mode - Trimmable / AOT‑friendly, XML‑documented public API, MIT licensed
- Installation
- Quick start
- Theming & dark mode
- Utilities (
Cn.Merge,Cva) - Component catalog
- Lucide icons
- Render modes & AOT
- Project structure
- Development
- AI coding skill (Claude Code)
- License
dotnet add package BlazorCNBlazorCN ships one DI helper that wires up the JS interop and toast service:
// Program.cs
using BlazorCN;
builder.Services.AddBlazorCN(); // registers JsInteropCn + ToastService (scoped)Add the CSS‑variable theme (provides --background, --primary, .dark, etc.) to your host page (index.html, App.razor, or _Host.cshtml):
<link rel="stylesheet" href="_content/BlazorCN/blazorcn.css" />
<link rel="stylesheet" href="app.css" /> <!-- your compiled Tailwind output -->The JS interop module (
_content/BlazorCN/blazorcn.js) is imported on demand — no<script>tag required.
BlazorCN outputs Tailwind utility classes, so your app needs Tailwind. Create a Styles/app.css:
@import "tailwindcss";
@import "tw-animate-css";
/* BlazorCN component classes (must be processed by Tailwind for @apply) */
@import "../_content/BlazorCN/blazorcn-components.css";
/* Tailwind must "see" the utility classes the components emit. Scan your own
markup, and point @source at the BlazorCN component sources you reference. */
@source "../Components/**/*.razor"; /* your app's components */
@custom-variant dark (&:is(.dark *));
/* Map the shadcn CSS variables to Tailwind tokens */
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
/* ...full token list in docs/BlazorCN.Demo/Styles/app.css... */
}Build it:
npx @tailwindcss/cli -i ./Styles/app.css -o ./wwwroot/app.css --minifyA ready‑made Tailwind preset is shipped at
_content/BlazorCN/tailwind-preset.jsfor JS‑config setups. The complete, working reference setup lives indocs/BlazorCN.Demo.
BlazorCN uses a single flat namespace:
@* _Imports.razor *@
@using BlazorCN@using BlazorCN
<CardCn Class="w-[380px]">
<CardHeaderCn>
<CardTitleCn>Create project</CardTitleCn>
<CardDescriptionCn>Deploy your new project in one click.</CardDescriptionCn>
</CardHeaderCn>
<CardContentCn>
<div class="flex flex-col gap-2">
<LabelCn For="name">Name</LabelCn>
<InputCn id="name" Placeholder="Name of your project" @bind-Value="_name" />
</div>
</CardContentCn>
<CardFooterCn Class="justify-between">
<ButtonCn Variant="ButtonVariant.Outline">Cancel</ButtonCn>
<ButtonCn OnClick="Deploy">Deploy</ButtonCn>
</CardFooterCn>
</CardCn>
@code {
private string? _name;
private void Deploy() { /* ... */ }
}Every component is suffixed with Cn (e.g. ButtonCn, DialogCn) and accepts:
Class— extra Tailwind classes, intelligently merged viaCn.Merge()Style— inline styles- arbitrary attributes (
id,data-*,aria-*, …) — captured and forwarded
Interactive components expose typed callbacks — e.g. ButtonCn has OnClick as EventCallback<MouseEventArgs>, so both OnClick="Handler" and @onclick="Handler" work.
Theming is the exact shadcn/ui system: semantic CSS variables defined on :root and .dark in _content/BlazorCN/blazorcn.css. Override them anywhere to retheme:
:root {
--primary: oklch(0.205 0 0);
--radius: 0.65rem;
}Toggle dark mode by adding/removing the dark class on <html>:
await JS.InvokeVoidAsync("document.documentElement.classList.toggle", "dark");Resolves conflicting Tailwind utilities (last one wins per group) so consumer Class overrides behave intuitively:
Cn.Merge("px-2 py-1 bg-red-500", "bg-blue-500"); // → "px-2 py-1 bg-blue-500"Type‑safe variant → class maps, used internally by components and available to yours:
private static readonly Cva<ButtonVariant, ButtonSize> Variants = new(
baseClasses: "inline-flex items-center justify-center rounded-lg ...",
variants: new() { [ButtonVariant.Default] = "bg-primary text-primary-foreground", ... },
sizes: new() { [ButtonSize.Default] = "h-8 px-4", ... });~200 components in 55 groups. Browse them live with
docs/BlazorCN.Demoat/docs/components/{name}.
| Group | Components | Description |
|---|---|---|
| AspectRatio | AspectRatioCn |
Constrain content to a width/height ratio |
| Card | CardCn + Header/Title/Description/Action/Content/Footer |
Content container |
| Separator | SeparatorCn |
Horizontal/vertical divider |
| ScrollArea | ScrollAreaCn, ScrollBarCn |
Scroll container |
| Resizable | ResizablePanelGroupCn, ResizablePanelCn, ResizableHandleCn |
Resizable split panes |
| Sidebar | SidebarProviderCn, SidebarCn + 20 parts |
Collapsible app sidebar |
| Item | ItemCn + Group/Header/Media/Content/Title/Description/Actions/Footer/Separator |
List‑item primitive |
| Empty | EmptyCn + Header/Media/Title/Description/Content |
Empty‑state placeholder |
| Field | FieldCn/FieldSetCn + Legend/Group/Label/Title/Description/Error/Content/Separator |
Form field layout |
| Table | TableCn + Header/Body/Footer/Row/Head/Cell/Caption |
Data table primitives |
| Group | Components | Description |
|---|---|---|
| Button | ButtonCn |
6 variants × 8 sizes; Href renders an anchor |
| ButtonGroup | ButtonGroupCn, ButtonGroupSeparatorCn, ButtonGroupTextCn |
Connected button row/column |
| Input | InputCn |
Text input, @bind-Value |
| InputGroup | InputGroupCn + Addon/Input/Textarea/Button/Text |
Input with addons |
| InputOtp | InputOtpCn, InputOtpGroupCn, InputOtpSlotCn, InputOtpSeparatorCn |
One‑time‑password input |
| Textarea | TextareaCn |
Multi‑line input |
| Label | LabelCn |
Accessible form label |
| Checkbox | CheckboxCn |
@bind-Value checkbox |
| RadioGroup | RadioGroupCn, RadioGroupItemCn |
Single‑select radios |
| Switch | SwitchCn |
Toggle switch |
| Select | SelectCn + Trigger/Value/Content/Group/Label/Item/Separator |
Custom dropdown select |
| NativeSelect | NativeSelectCn, NativeSelectOptionCn, NativeSelectOptGroupCn |
Native <select> |
| Combobox | ComboboxCn + Trigger/Input/Content/Group/Item/Empty/Separator |
Autocomplete select |
| Slider | SliderCn |
Range slider |
| Form | FormFieldCn, FormLabelCn, FormControlCn, FormDescriptionCn, FormMessageCn |
EditForm integration |
| Calendar | CalendarCn |
Date picker calendar (single/multiple/range) |
| Toggle | ToggleCn |
Two‑state toggle button |
| ToggleGroup | ToggleGroupCn, ToggleGroupItemCn |
Grouped toggles |
| Group | Components | Description |
|---|---|---|
| Breadcrumb | BreadcrumbCn + List/Item/Link/Page/Separator/Ellipsis |
Breadcrumb trail |
| NavigationMenu | NavigationMenuCn + List/Item/Trigger/Content/Link/Indicator/Viewport |
Top‑level nav with dropdowns |
| Pagination | PaginationCn + Content/Item/Link/Previous/Next/Ellipsis |
Page navigation |
| Menubar | MenubarCn + Menu/Trigger/Content/Item/Checkbox/Radio/Sub/Label/Separator/Shortcut |
Desktop‑style menu bar |
| Tabs | TabsCn, TabsListCn, TabsTriggerCn, TabsContentCn |
Tabbed panels (default & line variants) |
| Group | Components | Description |
|---|---|---|
| Dialog | DialogCn + Trigger/Content/Header/Title/Description/Footer/Overlay/Close |
Modal dialog |
| AlertDialog | AlertDialogCn + Trigger/Content/Header/Title/Description/Footer/Action/Cancel/Overlay |
Confirmation dialog |
| Sheet | SheetCn + Trigger/Content/Header/Title/Description/Footer/Overlay/Close |
Side panel |
| Drawer | DrawerCn + Trigger/Content/Header/Title/Description/Footer/Overlay/Close |
Bottom drawer |
| Popover | PopoverCn + Trigger/Anchor/Content/Header/Title/Description |
Floating popover |
| HoverCard | HoverCardCn, HoverCardTriggerCn, HoverCardContentCn |
Hover‑to‑reveal card |
| Tooltip | TooltipCn, TooltipTriggerCn, TooltipContentCn |
Tooltip |
| DropdownMenu | DropdownMenuCn + 13 parts (items, checkbox/radio, sub‑menus, shortcuts) |
Action menu |
| ContextMenu | ContextMenuCn + 13 parts |
Right‑click menu |
| Command | CommandCn + Input/List/Group/Item/Empty/Separator |
Command palette |
| Group | Components | Description |
|---|---|---|
| Alert | AlertCn, AlertTitleCn, AlertDescriptionCn |
Inline alert |
| Badge | BadgeCn |
Status badge |
| Progress | ProgressCn |
Progress bar |
| Skeleton | SkeletonCn |
Loading placeholder |
| Spinner | SpinnerCn |
Loading spinner |
| Toast | ToasterCn, ToastCn + ToastService |
Toast notifications (call ToastService.Success(...)) |
| Kbd | KbdCn |
Keyboard shortcut hint |
| Group | Components | Description |
|---|---|---|
| Avatar | AvatarCn + Image/Fallback/Badge/Group/GroupCount |
User avatar & stacks |
| Accordion | AccordionCn, AccordionItemCn, AccordionTriggerCn, AccordionContentCn |
Collapsible sections |
| Collapsible | CollapsibleCn, CollapsibleTriggerCn, CollapsibleContentCn |
Single collapsible region |
| Carousel | CarouselCn + Content/Item/Previous/Next |
Horizontal/vertical carousel |
| Chart | ChartCn, ChartContainerCn |
Chart container/theming |
Composed recipes (built from the primitives above, see the demo): Date Picker =
Calendar+Popover; Data Table =Table+ sorting/filtering; File Upload =InputGroup+Item.
All 1,702 Lucide icons ship as components, e.g.:
<LucideCheckCn Size="16" Class="text-green-600" />
<LucideTriangleAlertCn />Each accepts Size, Fill, Stroke, StrokeWidth, Class, Style.
Or pick an icon by name with the dispatcher (handles lucide v1→v2 renames):
<LucideIconCn Name="circle-check-big" Size="20" Class="text-primary" />The dispatcher resolves icons via reflection. For Native‑AOT / aggressive trimming, prefer the concrete
Lucide{Name}Cncomponent so the icon type is statically referenced.
Works under Server, WebAssembly, Auto, and Static SSR. JS interop (Floating‑UI positioning, focus trap, scroll lock) is loaded lazily and degrades gracefully during prerender.
For Native‑AOT/trimmed WASM: JS‑interop option objects use plain classes with init‑only properties (not records) for source‑generated serialization. Reference concrete icon components rather than the by‑name dispatcher.
blazorcn/
├── src/BlazorCN/
│ ├── ComponentBaseCn.cs # base: Class, Style, AdditionalAttributes
│ ├── ServiceCollectionExtensions.cs # AddBlazorCN()
│ ├── JsInteropCn.cs # typed JS interop wrapper
│ ├── Utilities/{Cn.cs, Cva.cs} # tailwind-merge + CVA ports
│ ├── Components/ # 55 groups + LucideIcon/Icons (1702)
│ └── wwwroot/{blazorcn.css, blazorcn-components.css, blazorcn.js, tailwind-preset.js}
├── tests/BlazorCN.Tests/ # bUnit + xUnit
└── docs/BlazorCN.Demo/ # reference consumer app + live docs
dotnet build # build library
dotnet test # run bUnit/xUnit suite
dotnet pack src/BlazorCN/BlazorCN.csproj -c Release # produce NuGet package
# Run the docs/demo app
cd docs/BlazorCN.Demo
npm install && npm run dev:css # Tailwind watch (separate terminal)
dotnet run # serves the component galleryThis repo ships an Agent Skill at skills/blazorcn/ that teaches AI
assistants (Claude Code, and any agent that reads skill markdown) to write correct BlazorCN —
component composition, @bind-Value/@bind-Open/@bind-Checked, variants/enums, Lucide icons,
Tailwind setup, CSS‑variable theming, and porting shadcn React snippets to Blazor. It auto‑activates
when you work on Blazor/Razor code that uses BlazorCN (no need to invoke it manually). In an internal
benchmark it took model‑written BlazorCN from ~67% to ~99% rule‑correct.
Install it in your consuming app (or globally), not in this library repo. Pick one method.
The repo doubles as a single‑plugin marketplace (.claude-plugin/). In Claude Code:
/plugin marketplace add evisoft/blazorcn
/plugin install blazorcn@blazorcn
Already cloned the repo? Point the marketplace at your local checkout instead of GitHub:
/plugin marketplace add C:\path\to\blazorcn # or: /plugin marketplace add ./
/plugin install blazorcn@blazorcn
The skill activates automatically; enable it if Claude Code prompts you. Update later with
/plugin marketplace update blazorcn.
Copy (or symlink) the skill folder into a Claude skills directory — ~/.claude/skills/ for all
projects, or <your-app>/.claude/skills/ for one project. Run these from a clone of this repo:
# Windows PowerShell — personal scope (all projects)
New-Item -ItemType Directory -Force "$env:USERPROFILE\.claude\skills" | Out-Null
Copy-Item -Recurse -Force .\skills\blazorcn "$env:USERPROFILE\.claude\skills\blazorcn"# macOS / Linux — personal scope (all projects)
mkdir -p ~/.claude/skills
cp -r skills/blazorcn ~/.claude/skills/blazorcnFor project scope, replace the destination with <your-app>/.claude/skills/blazorcn. To stay in
sync with the repo, symlink instead of copying:
ln -s "$(pwd)/skills/blazorcn" ~/.claude/skills/blazorcn # macOS/LinuxNew-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.claude\skills\blazorcn" `
-Target (Resolve-Path .\skills\blazorcn) # Windows (Dev Mode/admin)The skill is plain markdown. Drop skills/blazorcn/ where your agent discovers skills, or point it at
skills/blazorcn/SKILL.md as context — the body links the detailed
references (setup.md, customization.md, components.md, rules/*.md) via progressive disclosure.
Keep the skill in sync with the BlazorCN package version you consume — it documents the API exactly.
MIT © BlazorCN Contributors. Lucide icons are ISC licensed. Design and component APIs mirror shadcn/ui (MIT).
