<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jimmy Petrus</title>
    <description>The latest articles on DEV Community by Jimmy Petrus (@jimmyps).</description>
    <link>https://dev.to/jimmyps</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3843133%2F692537f9-d910-43f0-909d-7b786421db97.jpg</url>
      <title>DEV Community: Jimmy Petrus</title>
      <link>https://dev.to/jimmyps</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jimmyps"/>
    <language>en</language>
    <item>
      <title>The beauty of component composability — and why NeoUI does it differently</title>
      <dc:creator>Jimmy Petrus</dc:creator>
      <pubDate>Sat, 28 Mar 2026 06:08:44 +0000</pubDate>
      <link>https://dev.to/jimmyps/the-beauty-of-component-composability-and-why-neoui-does-it-differently-5hj9</link>
      <guid>https://dev.to/jimmyps/the-beauty-of-component-composability-and-why-neoui-does-it-differently-5hj9</guid>
      <description>&lt;p&gt;There's a question every UI library author eventually has to answer: when you need to add a new capability to an existing component, what do you do?&lt;/p&gt;

&lt;p&gt;It sounds simple. But how you answer it reveals everything about how your library is architected — and whether it will age well or collapse under its own weight.&lt;/p&gt;

&lt;p&gt;NeoUI v3.8.0 ships a new &lt;code&gt;Sortable&lt;/code&gt; drag-and-drop component. The way it was built is a better answer to that question than most UI libraries give — and a recent refinement takes the idea one step further. Let me show you why.&lt;/p&gt;




&lt;h2&gt;
  
  
  The traditional approach — and where it breaks
&lt;/h2&gt;

&lt;p&gt;Let's say you have a list view component. It renders a list of items. Works great.&lt;/p&gt;

&lt;p&gt;A designer asks: &lt;em&gt;"Can we make the items draggable to reorder?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In most UI libraries, you have two options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Add the feature directly to the component.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;Draggable&lt;/code&gt; prop. Add a &lt;code&gt;OnReorder&lt;/code&gt; event. Add drag handle rendering logic. Add drag-over state. Add keyboard handling. Add the overlay. Now your &lt;code&gt;ListView&lt;/code&gt; has grown a second responsibility — it's both a list renderer and a drag-and-drop manager. Every future capability request adds another flag, another event, another conditional render path. Two years later your component has 40 parameters and a 2,000-line implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B: Create a new variant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ship a &lt;code&gt;SortableListView&lt;/code&gt;. Now you have &lt;code&gt;ListView&lt;/code&gt; and &lt;code&gt;SortableListView&lt;/code&gt; — two components to maintain, two sets of documentation, two places to fix bugs, two implementations to keep in sync. And when the next request comes — &lt;em&gt;"Can the data table rows also be sortable?"&lt;/em&gt; — you're back to the same decision: add it to &lt;code&gt;DataTable&lt;/code&gt;, or ship a &lt;code&gt;SortableDataTable&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;This is how UI libraries end up with enormous surface areas and inconsistent behaviour across component variants. It's also how they become resistant to change — every new capability requires modifying existing code rather than composing new behaviour alongside it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter composability
&lt;/h2&gt;

&lt;p&gt;The shadcn/ui philosophy that NeoUI is built on says something different: &lt;strong&gt;capabilities should be composable around components, not baked into them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rather than modifying &lt;code&gt;ListView&lt;/code&gt; or &lt;code&gt;DataTable&lt;/code&gt; to support sorting, you build a &lt;code&gt;Sortable&lt;/code&gt; wrapper that can be layered &lt;em&gt;around&lt;/em&gt; any component. The existing components stay unchanged. The new behaviour lives in new code. And the same &lt;code&gt;Sortable&lt;/code&gt; works for every list-like component — today and in the future.&lt;/p&gt;

&lt;p&gt;This is exactly how NeoUI's new &lt;code&gt;Sortable&lt;/code&gt; component works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sortable in NeoUI — the basic case
&lt;/h2&gt;

&lt;p&gt;The simplest use case looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Sortable TItem="TaskItem"
          Items="@items"
          OnItemsReordered="@(r =&amp;gt; items = r)"
          GetItemId="@(i =&amp;gt; i.Id)"&amp;gt;
    &amp;lt;SortableContent&amp;gt;
        @foreach (var item in items)
        {
            &amp;lt;SortableItem Value="@item.Id"&amp;gt;
                &amp;lt;SortableItemHandle /&amp;gt;
                &amp;lt;span class="flex-1 text-sm font-medium select-none"&amp;gt;@item.Name&amp;lt;/span&amp;gt;
            &amp;lt;/SortableItem&amp;gt;
        }
    &amp;lt;/SortableContent&amp;gt;
    &amp;lt;SortableOverlay /&amp;gt;
&amp;lt;/Sortable&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Sortable&lt;/code&gt; wraps your content. &lt;code&gt;SortableContent&lt;/code&gt; defines the droppable region. &lt;code&gt;SortableItem&lt;/code&gt; marks each draggable element. &lt;code&gt;SortableItemHandle&lt;/code&gt; is the grip — the six-dot handle users grab. &lt;code&gt;SortableOverlay&lt;/code&gt; is the floating ghost shown while dragging.&lt;/p&gt;

&lt;p&gt;When a drag completes, &lt;code&gt;OnItemsReordered&lt;/code&gt; fires with the new ordered list. Your state updates. Blazor re-renders. Done.&lt;/p&gt;

&lt;p&gt;The interaction model is complete: pointer, touch, and full keyboard support (&lt;code&gt;Space&lt;/code&gt;/&lt;code&gt;Enter&lt;/code&gt; to grab, arrow keys to move, &lt;code&gt;Escape&lt;/code&gt; to cancel) — all built into the primitive layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Now the interesting part — composing with DataTable
&lt;/h2&gt;

&lt;p&gt;Here's where the design philosophy becomes visible. The question was: how do you make &lt;code&gt;DataTable&lt;/code&gt; rows sortable?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;not&lt;/strong&gt; to add drag-and-drop support to &lt;code&gt;DataTable&lt;/code&gt;. Instead, you wrap &lt;code&gt;DataTable&lt;/code&gt; with &lt;code&gt;Sortable&lt;/code&gt;. &lt;code&gt;DataTable&lt;/code&gt; exposes &lt;code&gt;AdditionalRowAttributes&lt;/code&gt; — a general-purpose hook for stamping attributes onto each rendered row — and &lt;code&gt;Sortable&lt;/code&gt; uses it to wire up drag identity. Neither component knows anything about the other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Sortable TItem="TaskItem"
          Items="@items"
          OnItemsReordered="@(r =&amp;gt; items = r)"
          GetItemId="@(i =&amp;gt; i.Id)"
          Context="s"&amp;gt;
    &amp;lt;SortableContent Class="block"&amp;gt;
        &amp;lt;DataTable TData="TaskItem" Data="@items"
                   AdditionalRowAttributes="@s.RowAttributes"
                   ShowPagination="false"
                   ShowToolbar="false"&amp;gt;
            &amp;lt;Columns&amp;gt;
                &amp;lt;DataTableColumn TData="TaskItem" TValue="string"
                                 Property="@(i =&amp;gt; i.Id)"
                                 Header=""
                                 Width="40px"&amp;gt;
                    &amp;lt;CellTemplate Context="row"&amp;gt;
                        &amp;lt;SortableItemHandle Class="mx-auto" /&amp;gt;
                    &amp;lt;/CellTemplate&amp;gt;
                &amp;lt;/DataTableColumn&amp;gt;
                &amp;lt;DataTableColumn TData="TaskItem" TValue="string"
                                 Property="@(i =&amp;gt; i.Name)"
                                 Header="Task" /&amp;gt;
                &amp;lt;DataTableColumn TData="TaskItem" TValue="string"
                                 Property="@(i =&amp;gt; i.Status)"
                                 Header="Status" /&amp;gt;
            &amp;lt;/Columns&amp;gt;
        &amp;lt;/DataTable&amp;gt;
    &amp;lt;/SortableContent&amp;gt;
    &amp;lt;SortableOverlay Class="rounded" /&amp;gt;
&amp;lt;/Sortable&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what &lt;strong&gt;didn't&lt;/strong&gt; change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DataTable&lt;/code&gt; has no &lt;code&gt;Draggable&lt;/code&gt; flag&lt;/li&gt;
&lt;li&gt;There's no &lt;code&gt;SortableDataTable&lt;/code&gt; variant&lt;/li&gt;
&lt;li&gt;Column definitions, sorting, selection, toolbar — all work exactly as before&lt;/li&gt;
&lt;li&gt;The drag handle lives in a normal &lt;code&gt;CellTemplate&lt;/code&gt; using the existing Columns API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;Sortable&lt;/code&gt; does all the heavy lifting. &lt;code&gt;SortableItemHandle&lt;/code&gt; wires itself through the primitive's context — drop it in a &lt;code&gt;CellTemplate&lt;/code&gt; and it just works, the same way placing a &lt;code&gt;SortableItemHandle&lt;/code&gt; in any other template gives you a grip. &lt;code&gt;DataTable&lt;/code&gt; is entirely unaware any of this is happening.&lt;/p&gt;




&lt;h2&gt;
  
  
  Taking composability further — &lt;code&gt;SortableScope&amp;lt;TItem&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This works. But there's still something slightly awkward: the consumer has to manually construct the attributes dictionary and pass it to &lt;code&gt;AdditionalRowAttributes&lt;/code&gt;. That's an implementation concern of &lt;code&gt;Sortable&lt;/code&gt; leaking into the consumer's code.&lt;/p&gt;

&lt;p&gt;This is the kind of friction composable APIs should eliminate. So we took it a step further.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Sortable&amp;lt;TItem&amp;gt;&lt;/code&gt; provides a &lt;code&gt;SortableScope&amp;lt;TItem&amp;gt;&lt;/code&gt; through its &lt;code&gt;ChildContent&lt;/code&gt; — a typed context following the same Blazor-idiomatic pattern as &lt;code&gt;EditForm&lt;/code&gt; with &lt;code&gt;Context="ctx"&lt;/code&gt; or &lt;code&gt;QuickGrid&lt;/code&gt; with &lt;code&gt;Context="item"&lt;/code&gt;. It's opt-in: you only add &lt;code&gt;Context="s"&lt;/code&gt; when you need what the scope exposes. Without it, &lt;code&gt;Sortable&lt;/code&gt; works exactly as shown in the basic examples above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Sortable TItem="TaskItem"
          Items="@items"
          OnItemsReordered="@(r =&amp;gt; items = r)"
          GetItemId="@(i =&amp;gt; i.Id)"
          Context="s"&amp;gt;
    &amp;lt;SortableContent Class="block"&amp;gt;
        &amp;lt;DataTable TData="TaskItem" Data="@items"
                   AdditionalRowAttributes="@s.RowAttributes"
                   ShowPagination="false"
                   ShowToolbar="false"&amp;gt;
            &amp;lt;Columns&amp;gt;
                &amp;lt;DataTableColumn TData="TaskItem" TValue="string"
                                 Property="@(i =&amp;gt; i.Id)"
                                 Header="" Width="40px"&amp;gt;
                    &amp;lt;CellTemplate Context="row"&amp;gt;
                        &amp;lt;SortableItemHandle Class="mx-auto" /&amp;gt;
                    &amp;lt;/CellTemplate&amp;gt;
                &amp;lt;/DataTableColumn&amp;gt;
                &amp;lt;DataTableColumn TData="TaskItem" TValue="string"
                                 Property="@(i =&amp;gt; i.Name)"
                                 Header="Task" /&amp;gt;
                &amp;lt;DataTableColumn TData="TaskItem" TValue="string"
                                 Property="@(i =&amp;gt; i.Status)"
                                 Header="Status" /&amp;gt;
            &amp;lt;/Columns&amp;gt;
        &amp;lt;/DataTable&amp;gt;
    &amp;lt;/SortableContent&amp;gt;
    &amp;lt;SortableOverlay Class="rounded" /&amp;gt;
&amp;lt;/Sortable&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;s.RowAttributes&lt;/code&gt; is all you pass to &lt;code&gt;AdditionalRowAttributes&lt;/code&gt;. Everything &lt;code&gt;Sortable&lt;/code&gt; needs to track each row is encapsulated inside it — the consumer never sees the wiring. The same applies to &lt;code&gt;SortableItemHandle&lt;/code&gt; — drop it anywhere in your template and it finds its context automatically. You don't configure it, you don't pass IDs to it. It just works.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SortableScope&amp;lt;TItem&amp;gt;&lt;/code&gt; exposes three properties:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RowAttributes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Func&amp;lt;TItem, Dictionary&amp;lt;string, object&amp;gt;?&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sortable attributes for each row. Pass directly to &lt;code&gt;AdditionalRowAttributes&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ActiveId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;string?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The ID of the item currently being dragged. &lt;code&gt;null&lt;/code&gt; when not dragging.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IsDragging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt; while any drag is in progress.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IsItemDragging(item)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns &lt;code&gt;true&lt;/code&gt; if the given item is the one currently being dragged.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last three are bonus capabilities — they give you live drag state in your template with zero extra event wiring. Want to dim non-dragged items during a drag? Render a custom placeholder? Show a drag count badge?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Sortable TItem="TaskItem"
          Items="@items"
          OnItemsReordered="@(r =&amp;gt; items = r)"
          GetItemId="@(i =&amp;gt; i.Id)"
          Context="s"&amp;gt;
    &amp;lt;SortableContent&amp;gt;
        @foreach (var item in items)
        {
            &amp;lt;SortableItem Value="@item.Id"
                          Class="@(s.IsItemDragging(item) ? "opacity-40" : "")"&amp;gt;
                &amp;lt;SortableItemHandle /&amp;gt;
                &amp;lt;span class="flex-1"&amp;gt;@item.Name&amp;lt;/span&amp;gt;
                @if (s.IsDragging)
                {
                    &amp;lt;Badge Variant="BadgeVariant.Secondary"&amp;gt;Moving...&amp;lt;/Badge&amp;gt;
                }
            &amp;lt;/SortableItem&amp;gt;
        }
    &amp;lt;/SortableContent&amp;gt;
    &amp;lt;SortableOverlay /&amp;gt;
&amp;lt;/Sortable&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And critically — &lt;code&gt;s.RowAttributes&lt;/code&gt; is not &lt;code&gt;DataTable&lt;/code&gt;-specific. Any future component that exposes a row/item attribute hook works the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Sortable TItem="TaskItem" ... Context="s"&amp;gt;
    &amp;lt;SortableContent Class="block"&amp;gt;
        &amp;lt;DataTable AdditionalRowAttributes="@s.RowAttributes" ... /&amp;gt;
        &amp;lt;MyFutureGrid RowAttrs="@s.RowAttributes" ... /&amp;gt;   &amp;lt;!-- works too --&amp;gt;
    &amp;lt;/SortableContent&amp;gt;
&amp;lt;/Sortable&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Sortable&lt;/code&gt; doesn't know what &lt;code&gt;DataTable&lt;/code&gt; is. &lt;code&gt;DataTable&lt;/code&gt; doesn't know what &lt;code&gt;Sortable&lt;/code&gt; is. &lt;code&gt;SortableScope&lt;/code&gt; is the typed bridge between them — and it works for every downstream component, forever, without either side needing to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same pattern with DataView
&lt;/h2&gt;

&lt;p&gt;The same composability applies to &lt;code&gt;DataView&lt;/code&gt; — and &lt;code&gt;DataView&lt;/code&gt; required &lt;strong&gt;zero changes&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Sortable TItem="ProjectItem"
          Items="@items"
          OnItemsReordered="@(r =&amp;gt; items = r)"
          GetItemId="@(i =&amp;gt; i.Id)"&amp;gt;
    &amp;lt;SortableContent Class="block"&amp;gt;
        &amp;lt;DataView TItem="ProjectItem" Items="@items"&amp;gt;
            &amp;lt;ListTemplate Context="item"&amp;gt;
                &amp;lt;SortableItem Value="@item.Id" Class="mb-2"&amp;gt;
                    &amp;lt;SortableItemHandle /&amp;gt;
                    &amp;lt;Item&amp;gt;
                        &amp;lt;ItemContent&amp;gt;
                            &amp;lt;ItemTitle&amp;gt;@item.Name&amp;lt;/ItemTitle&amp;gt;
                            &amp;lt;ItemDescription&amp;gt;@item.Description&amp;lt;/ItemDescription&amp;gt;
                        &amp;lt;/ItemContent&amp;gt;
                    &amp;lt;/Item&amp;gt;
                &amp;lt;/SortableItem&amp;gt;
            &amp;lt;/ListTemplate&amp;gt;
        &amp;lt;/DataView&amp;gt;
    &amp;lt;/SortableContent&amp;gt;
    &amp;lt;SortableOverlay /&amp;gt;
&amp;lt;/Sortable&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place &lt;code&gt;&amp;lt;SortableItem&amp;gt;&lt;/code&gt; directly inside &lt;code&gt;&amp;lt;ListTemplate&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;GridTemplate&amp;gt;&lt;/code&gt;. That's it. &lt;code&gt;DataView&lt;/code&gt; doesn't know anything about drag-and-drop. It doesn't need to.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture behind this: the two-layer model
&lt;/h2&gt;

&lt;p&gt;NeoUI's composability works because every complex component is built in two layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sortable              → Styled wrapper with sensible defaults
SortablePrimitive     → Headless, zero visual opinion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;headless primitive&lt;/strong&gt; handles all the hard parts: pointer capture, touch events, keyboard navigation, focus management, ARIA attributes, drag overlay positioning, and the reorder algorithm. It exposes only the behaviour, with zero styling attached.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;styled component&lt;/strong&gt; layers pre-built CSS on top — the &lt;code&gt;flex flex-col gap-2&lt;/code&gt; layout, the &lt;code&gt;rounded-lg border bg-card&lt;/code&gt; item styling, the &lt;code&gt;shadow-lg opacity-90 scale-[1.05]&lt;/code&gt; overlay animation. Sensible defaults for the common case.&lt;/p&gt;

&lt;p&gt;When you need full control, you escape to the primitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;SortablePrimitive TItem="MyItem"
                   Items="@items"
                   OnItemsReordered="@(r =&amp;gt; items = r)"
                   GetItemId="@(i =&amp;gt; i.Id)"&amp;gt;
    &amp;lt;SortableContentPrimitive class="flex flex-col gap-2"&amp;gt;
        @foreach (var item in items)
        {
            &amp;lt;SortableItemPrimitive Value="@item.Id"
                                   class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3"&amp;gt;
                &amp;lt;SortableItemHandlePrimitive class="cursor-grab active:cursor-grabbing text-muted-foreground" /&amp;gt;
                &amp;lt;span class="flex-1 text-sm font-medium select-none"&amp;gt;@item.Name&amp;lt;/span&amp;gt;
            &amp;lt;/SortableItemPrimitive&amp;gt;
        }
    &amp;lt;/SortableContentPrimitive&amp;gt;
    &amp;lt;SortableOverlayPrimitive class="rounded-lg shadow-lg opacity-90
                                     data-[state=dragging]:scale-[1.05]
                                     transition-transform duration-150" /&amp;gt;
&amp;lt;/SortablePrimitive&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every CSS class here is yours. The primitive supplies the behaviour, accessibility, and interaction model. You supply the appearance.&lt;/p&gt;




&lt;h2&gt;
  
  
  The overlay: a detail worth appreciating
&lt;/h2&gt;

&lt;p&gt;The drag overlay — the ghost that floats under your cursor while dragging — is handled through &lt;code&gt;SortableOverlay&lt;/code&gt; / &lt;code&gt;SortableOverlayPrimitive&lt;/code&gt;. The implementation is worth understanding because it illustrates another NeoUI design choice.&lt;/p&gt;

&lt;p&gt;By default, when &lt;code&gt;ChildContent&lt;/code&gt; is null, the JS sensor auto-clones the dragged element (&lt;code&gt;cloneNode(true)&lt;/code&gt;) into the overlay frame. The clone fills the frame at 100×100% and carries over the source element's styles — so the ghost looks exactly like the item being dragged, with no extra work.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; elements (DataTable rows), the sensor snapshots each &lt;code&gt;td&lt;/code&gt;/&lt;code&gt;th&lt;/code&gt; computed width &lt;em&gt;before&lt;/em&gt; cloning and stamps it as an inline &lt;code&gt;width&lt;/code&gt; on the clone cells. This preserves the shared table layout geometry even though the clone lives outside its parent &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;. It's a subtle problem and a clean solution.&lt;/p&gt;

&lt;p&gt;Visual effects — shadow, opacity, scale — live entirely in CSS via the &lt;code&gt;data-[state=dragging]&lt;/code&gt; attribute. JS sets &lt;code&gt;data-state="dragging"&lt;/code&gt; after positioning the overlay; Tailwind's data-attribute variants do the rest. No JS inline transforms. No animation logic in JavaScript.&lt;/p&gt;

&lt;p&gt;When you want a fully custom ghost, provide a &lt;code&gt;ChildContent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;SortableOverlay&amp;gt;
    @* ChildContent context is the active item ID *@
    &amp;lt;SortableOverlay ChildContent="@(activeId =&amp;gt; @&amp;lt;div class="my-custom-ghost"&amp;gt;
        Dragging: @activeId
    &amp;lt;/div&amp;gt;)" /&amp;gt;
&amp;lt;/SortableOverlay&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why this matters beyond drag-and-drop
&lt;/h2&gt;

&lt;p&gt;The Sortable component is a concrete example of a principle that runs through every NeoUI component.&lt;/p&gt;

&lt;p&gt;When v3.6.0 shipped &lt;code&gt;Timeline&lt;/code&gt;, it didn't add timeline rendering to &lt;code&gt;Card&lt;/code&gt; or create a &lt;code&gt;TimelineCard&lt;/code&gt; variant. Timeline is its own composable set of sub-components — &lt;code&gt;TimelineItem&lt;/code&gt;, &lt;code&gt;TimelineHeader&lt;/code&gt;, &lt;code&gt;TimelineConnector&lt;/code&gt;, &lt;code&gt;TimelineContent&lt;/code&gt; — that you assemble however your layout requires.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;DynamicForm&lt;/code&gt; shipped, it didn't modify &lt;code&gt;Input&lt;/code&gt; or &lt;code&gt;Select&lt;/code&gt; or &lt;code&gt;DatePicker&lt;/code&gt;. It reads a schema and renders the existing components exactly as you'd write them by hand — because those components are already composable enough to be driven by data.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;DataView&lt;/code&gt; shipped with list/grid switching, it didn't rebuild table or card rendering. It exposed &lt;code&gt;ListTemplate&lt;/code&gt; and &lt;code&gt;GridTemplate&lt;/code&gt; slots where you compose existing components inside.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SortableScope&amp;lt;TItem&amp;gt;&lt;/code&gt; takes this a step further: it's not just composing components together, it's encapsulating the integration contract itself. The consumer gets a typed scope object rather than raw implementation details. &lt;code&gt;Sortable&lt;/code&gt; and &lt;code&gt;DataTable&lt;/code&gt; remain completely independent — &lt;code&gt;SortableScope&lt;/code&gt; is the only shared surface between them, and it's intentionally minimal.&lt;/p&gt;

&lt;p&gt;The pattern is always the same: &lt;strong&gt;new behaviour as new components, composed around existing ones, with minimal surgical hooks where genuine integration is required — and those hooks encapsulated so consumers never see the machinery.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is how a library stays maintainable as it grows. It's why NeoUI can ship 100+ components without each one becoming a monolith.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using Sortable in your project
&lt;/h2&gt;

&lt;p&gt;Available in &lt;code&gt;NeoUI.Blazor&lt;/code&gt; v3.8.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package NeoUI.Blazor &lt;span class="nt"&gt;--version&lt;/span&gt; 3.8.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The headless primitive is in &lt;code&gt;NeoUI.Blazor.Primitives&lt;/code&gt; (included transitively).&lt;/p&gt;

&lt;p&gt;Key parameters at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Items&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The list to sort. Required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GetItemId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extracts a unique string ID from each item. Required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnItemsReordered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fires after a drop with the new ordered list.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Orientation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Vertical&lt;/code&gt; (default), &lt;code&gt;Horizontal&lt;/code&gt;, &lt;code&gt;Grid&lt;/code&gt;, or &lt;code&gt;Mixed&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnDragStart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fires when drag begins. Receives the active item ID.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnDragEnd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fires when drag ends. Carries &lt;code&gt;ActiveId&lt;/code&gt;, &lt;code&gt;OverId&lt;/code&gt;, &lt;code&gt;FromIndex&lt;/code&gt;, &lt;code&gt;ToIndex&lt;/code&gt;, &lt;code&gt;Moved&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnDragCancel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fires when drag is cancelled (Escape or pointer cancel).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Live demos at &lt;a href="https://demos.neoui.io" rel="noopener noreferrer"&gt;demos.neoui.io&lt;/a&gt; cover vertical lists, horizontal chips, full-item drag, custom handle icons, &lt;code&gt;DataView&lt;/code&gt; composability, &lt;code&gt;DataTable&lt;/code&gt; integration, and the primitive escape hatch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Docs: &lt;a href="https://neoui.io" rel="noopener noreferrer"&gt;neoui.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎮 Live demo: &lt;a href="https://demos.neoui.io" rel="noopener noreferrer"&gt;demos.neoui.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 NuGet: &lt;a href="https://www.nuget.org/packages/NeoUI.Blazor/" rel="noopener noreferrer"&gt;NeoUI.Blazor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 GitHub: &lt;a href="https://github.com/jimmyps/blazor-shadcn-ui" rel="noopener noreferrer"&gt;github.com/jimmyps/blazor-shadcn-ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 X: &lt;a href="https://x.com/neoui_io" rel="noopener noreferrer"&gt;@neoui_io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;What capability would you want to compose onto your existing components next? Drop it in the comments — or open an issue on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>architecture</category>
      <category>composability</category>
    </item>
    <item>
      <title>I wanted shadcn/ui for Blazor. It didn’t exist. So I built it.</title>
      <dc:creator>Jimmy Petrus</dc:creator>
      <pubDate>Wed, 25 Mar 2026 14:03:11 +0000</pubDate>
      <link>https://dev.to/jimmyps/i-wanted-shadcnui-for-blazor-it-didnt-exist-so-i-built-it-369g</link>
      <guid>https://dev.to/jimmyps/i-wanted-shadcnui-for-blazor-it-didnt-exist-so-i-built-it-369g</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;This is the story of why I built NeoUI, what I learned, and how Blazor developers can now have the same thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shadcn/ui moment
&lt;/h2&gt;

&lt;p&gt;When shadcn/ui launched, it didn't just ship a set of components. It shipped a philosophy.&lt;/p&gt;

&lt;p&gt;The insight was simple but radical: &lt;strong&gt;you shouldn't be fighting your component library&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;shadcn/ui flipped this. Instead of locking you into a package you can't modify, it gives you the source. Components live in &lt;em&gt;your&lt;/em&gt; project. You own them. You change them however you want. There's no &lt;code&gt;node_modules&lt;/code&gt; black box to fight.&lt;/p&gt;

&lt;p&gt;The other half of the philosophy — &lt;strong&gt;composability over configuration&lt;/strong&gt; — 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Blazor gap
&lt;/h2&gt;

&lt;p&gt;Here's what the Blazor ecosystem looked like during that same period.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the package. Accept the design system. Style within their constraints.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MudBlazor looks like Material Design because it &lt;em&gt;is&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;What I couldn't find was anything that was simultaneously &lt;strong&gt;serious, well-architected, and comprehensive&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What NeoUI is (and isn't)
&lt;/h2&gt;

&lt;p&gt;NeoUI is &lt;strong&gt;not&lt;/strong&gt; a port of shadcn/ui. It's a Blazor component library that adopts shadcn/ui's design principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Composability over configuration&lt;/strong&gt; — components are built to be composed, not configured through a wall of parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You own your UI&lt;/strong&gt; — primitives give you headless behaviour you can style completely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero lock-in&lt;/strong&gt; — no mandatory design system, no "MudBlazor aesthetic"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-built is optional&lt;/strong&gt; — use the styled layer for speed, or drop to primitives for full control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And critically — it's &lt;strong&gt;fully compatible with shadcn/ui and tweakcn themes&lt;/strong&gt;. If you copy a theme from &lt;code&gt;ui.shadcn.com/themes&lt;/code&gt; and paste it into your &lt;code&gt;wwwroot/styles/theme.css&lt;/code&gt;, NeoUI uses it immediately. The same theme your React colleagues use. The same customisation tools. The same mental model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: the two-layer model
&lt;/h2&gt;

&lt;p&gt;The design that makes this possible is a two-package architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NeoUI.Blazor           → Styled components (pre-built CSS, ready to use)
NeoUI.Blazor.Primitives → Headless components (no CSS, full control)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The styled layer&lt;/strong&gt; is what most developers will use. Install &lt;code&gt;NeoUI.Blazor&lt;/code&gt;, add two lines to &lt;code&gt;App.razor&lt;/code&gt;, and you have 100+ production-ready components that look great out of the box. No Tailwind setup. No Node.js. No build tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The primitives layer&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;This is directly analogous to how Radix UI underlies shadcn/ui in the React world.&lt;/p&gt;




&lt;h2&gt;
  
  
  shadcn/ui vs NeoUI — side by side
&lt;/h2&gt;

&lt;p&gt;The parallel is easiest to see in code. Here's the same Dialog component in both ecosystems.&lt;/p&gt;

&lt;h3&gt;
  
  
  shadcn/ui (React)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DialogContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DialogHeader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DialogTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DialogDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DialogTrigger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;DialogClose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/ui/dialog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/ui/button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ConfirmDialog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogTrigger&lt;/span&gt; &lt;span class="na"&gt;asChild&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogTrigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Are you sure?&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogTitle&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            This action cannot be undone.
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogDescription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogHeader&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogFooter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DialogClose&lt;/span&gt; &lt;span class="na"&gt;asChild&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"outline"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Cancel&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogClose&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Continue&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogFooter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;DialogContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  NeoUI (Blazor)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Dialog&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DialogTrigger&lt;/span&gt; &lt;span class="na"&gt;AsChild&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Variant=&lt;/span&gt;&lt;span class="s"&gt;"ButtonVariant.Outline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/DialogTrigger&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DialogContent&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DialogHeader&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DialogTitle&amp;gt;&lt;/span&gt;Are you sure?&lt;span class="nt"&gt;&amp;lt;/DialogTitle&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DialogDescription&amp;gt;&lt;/span&gt;
                This action cannot be undone.
            &lt;span class="nt"&gt;&amp;lt;/DialogDescription&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/DialogHeader&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DialogFooter&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DialogClose&lt;/span&gt; &lt;span class="na"&gt;AsChild&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Variant=&lt;/span&gt;&lt;span class="s"&gt;"ButtonVariant.Outline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Cancel&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/DialogClose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;Button&amp;gt;&lt;/span&gt;Continue&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/DialogFooter&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/DialogContent&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structural parallel is intentional and precise. If you've used shadcn/ui, NeoUI feels immediately familiar. The same &lt;code&gt;AsChild&lt;/code&gt; pattern from Radix UI — allowing a trigger to render as your own element rather than a default button — is fully implemented.&lt;/p&gt;




&lt;h3&gt;
  
  
  Another example: Select
&lt;/h3&gt;

&lt;h4&gt;
  
  
  shadcn/ui (React)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SelectTrigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SelectValue&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Pick a framework"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SelectTrigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SelectContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SelectItem&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"blazor"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Blazor&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SelectItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SelectItem&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"react"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;React&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SelectItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SelectItem&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"vue"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Vue&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SelectItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SelectContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  NeoUI (Blazor)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Select&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;SelectTrigger&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;SelectValue&lt;/span&gt; &lt;span class="na"&gt;Placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Pick a framework"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/SelectTrigger&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;SelectContent&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;SelectItem&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"blazor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Blazor&lt;span class="nt"&gt;&amp;lt;/SelectItem&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;SelectItem&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"react"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;React&lt;span class="nt"&gt;&amp;lt;/SelectItem&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;SelectItem&lt;/span&gt; &lt;span class="na"&gt;Value=&lt;/span&gt;&lt;span class="s"&gt;"vue"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Vue&lt;span class="nt"&gt;&amp;lt;/SelectItem&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/SelectContent&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Select&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h3&gt;
  
  
  Headless primitive: Dialog
&lt;/h3&gt;

&lt;p&gt;When you need full control, drop to the primitive layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@using NeoUI.Blazor.Primitives.Dialog

&lt;span class="nt"&gt;&amp;lt;DialogPrimitive&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DialogPrimitiveTrigger&lt;/span&gt; &lt;span class="na"&gt;AsChild&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-custom-trigger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open settings&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/DialogPrimitiveTrigger&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DialogPrimitiveContent&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-dialog-panel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DialogPrimitiveTitle&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-dialog-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Settings
        &lt;span class="nt"&gt;&amp;lt;/DialogPrimitiveTitle&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Your completely custom markup and styles --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-dialog-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Everything here is yours. NeoUI handles focus trapping,
               Escape key, scroll lock, and ARIA — you handle the rest.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DialogPrimitiveClose&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"my-close-button"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;✕&lt;span class="nt"&gt;&amp;lt;/DialogPrimitiveClose&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/DialogPrimitiveContent&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/DialogPrimitive&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No NeoUI styles apply. No CSS variables referenced. The primitive gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Focus trapping inside the dialog&lt;/li&gt;
&lt;li&gt;Escape key to close&lt;/li&gt;
&lt;li&gt;Scroll lock on body&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role="dialog"&lt;/code&gt;, &lt;code&gt;aria-modal&lt;/code&gt;, &lt;code&gt;aria-labelledby&lt;/code&gt; wired automatically&lt;/li&gt;
&lt;li&gt;Keyboard navigation and screen reader support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You provide everything visual.&lt;/p&gt;




&lt;h2&gt;
  
  
  Theming: drop in any shadcn/ui theme
&lt;/h2&gt;

&lt;p&gt;This is one of NeoUI's most practical advantages. The entire ecosystem of shadcn/ui themes works out of the box.&lt;/p&gt;

&lt;p&gt;Go to &lt;a href="https://ui.shadcn.com/themes" rel="noopener noreferrer"&gt;ui.shadcn.com/themes&lt;/a&gt; or &lt;a href="https://tweakcn.com" rel="noopener noreferrer"&gt;tweakcn.com&lt;/a&gt;, pick or customise a theme, copy the CSS variables, and paste them into &lt;code&gt;wwwroot/styles/theme.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--primary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--secondary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.97&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--secondary-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.97&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--muted-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.556&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.97&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--accent-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.205&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--destructive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.577&lt;/span&gt; &lt;span class="m"&gt;0.245&lt;/span&gt; &lt;span class="m"&gt;27.325&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.922&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c"&gt;/* ... */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.dark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.145&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="py"&gt;--foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.985&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c"&gt;/* ... */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference it before NeoUI's CSS in &lt;code&gt;App.razor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"styles/theme.css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"@Assets["&lt;/span&gt;&lt;span class="na"&gt;_content&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;NeoUI.Blazor&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;components.css&lt;/span&gt;&lt;span class="err"&gt;"]"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"@Assets["&lt;/span&gt;&lt;span class="na"&gt;_content&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;NeoUI.Blazor&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;theme.js&lt;/span&gt;&lt;span class="err"&gt;"]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every NeoUI component picks up your theme variables automatically. Dark mode works by adding &lt;code&gt;.dark&lt;/code&gt; to &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; — no JavaScript required, no flicker (&lt;code&gt;theme.js&lt;/code&gt; applies the saved preference before Blazor loads, preventing FOUC).&lt;/p&gt;

&lt;p&gt;Beyond static themes, NeoUI ships a &lt;code&gt;ThemeService&lt;/code&gt; and &lt;code&gt;ThemeSwitcher&lt;/code&gt; component for runtime switching across 85 combinations — 5 base colours × 17 primary colours — all without a page reload.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's inside
&lt;/h2&gt;

&lt;p&gt;After 828 commits, NeoUI currently ships:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;100+ styled components&lt;/strong&gt; covering every UI pattern a real application needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forms: Button, Input, Select, Combobox, DatePicker, DateRangePicker, ColorPicker, MaskedInput, CurrencyInput, NumericInput, OTP, Rating, RangeSlider, MultiSelect and more&lt;/li&gt;
&lt;li&gt;Layout: Sidebar, Accordion, Tabs, Resizable, Carousel, NavigationMenu, Breadcrumb&lt;/li&gt;
&lt;li&gt;Overlays: Dialog, Sheet, Drawer, AlertDialog, ContextMenu, DropdownMenu, Toast, Popover, Tooltip, HoverCard, Command palette&lt;/li&gt;
&lt;li&gt;Data: DataTable (with sorting, filtering, column pinning, virtualisation, tree rows, server-side), Filter builder, RichTextEditor, MarkdownEditor&lt;/li&gt;
&lt;li&gt;Charts: 12 types including Candlestick, Gauge, Heatmap, Funnel, Radar&lt;/li&gt;
&lt;li&gt;Animation: Declarative Motion system with 20+ presets, scroll-triggered, spring physics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;15 headless primitives&lt;/strong&gt; for every complex interaction pattern&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3,200+ icons&lt;/strong&gt; across three packages: Lucide (1,640), Heroicons (1,288), Feather (286)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built-in localisation&lt;/strong&gt; via &lt;code&gt;ILocalizer&lt;/code&gt; — works across Server, WebAssembly, and Auto modes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full .NET 10 support&lt;/strong&gt; with Auto rendering mode — fast server-side initial load, seamless WebAssembly takeover&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;There are two paths depending on whether you're starting fresh or adding NeoUI to an existing project.&lt;/p&gt;

&lt;h3&gt;
  
  
  New project — use the template
&lt;/h3&gt;

&lt;p&gt;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 (&lt;code&gt;Ctrl+K&lt;/code&gt;), and a full Tailwind CSS v4 pipeline pre-wired — all supporting .NET 10's Auto, Server, or WebAssembly rendering modes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new &lt;span class="nb"&gt;install &lt;/span&gt;NeoUI.Blazor.Templates &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; dotnet new neoui &lt;span class="nt"&gt;-n&lt;/span&gt; MyApp
&lt;span class="nb"&gt;cd &lt;/span&gt;MyApp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; MyApp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Tailwind build runs automatically on every &lt;code&gt;dotnet build&lt;/code&gt; — Node.js just needs to be on your PATH. No manual configuration.&lt;/p&gt;

&lt;p&gt;The template supports all three rendering modes as a first-class option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new neoui &lt;span class="nt"&gt;-n&lt;/span&gt; MyApp                    &lt;span class="c"&gt;# Auto (default) — Server + WASM&lt;/span&gt;
dotnet new neoui &lt;span class="nt"&gt;-n&lt;/span&gt; MyApp &lt;span class="nt"&gt;-in&lt;/span&gt; Server         &lt;span class="c"&gt;# Server-side only, single project&lt;/span&gt;
dotnet new neoui &lt;span class="nt"&gt;-n&lt;/span&gt; MyApp &lt;span class="nt"&gt;-in&lt;/span&gt; WebAssembly    &lt;span class="c"&gt;# WebAssembly only&lt;/span&gt;
dotnet new neoui &lt;span class="nt"&gt;-n&lt;/span&gt; MyApp &lt;span class="nt"&gt;--empty&lt;/span&gt;            &lt;span class="c"&gt;# Auto, skip sample pages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you get out of the box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Sidebar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Collapsible sidebar with icon-only collapsed state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ThemeSwitcher&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime base color selector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DarkModeToggle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Light / dark / system mode toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SpotlightCommandPalette&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+K&lt;/code&gt; command palette&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ReconnectModal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Custom SignalR reconnect UI (Server + Auto)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AppLoader&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WASM initialization progress overlay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Auto and WebAssembly modes scaffold a two-project solution (&lt;code&gt;MyApp&lt;/code&gt; + &lt;code&gt;MyApp.Client&lt;/code&gt;). Server mode is a single project.&lt;/p&gt;




&lt;h3&gt;
  
  
  Existing project — manual install
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install the package&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package NeoUI.Blazor &lt;span class="nt"&gt;--version&lt;/span&gt; 3.6.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add namespace imports to &lt;code&gt;_Imports.razor&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@using NeoUI.Blazor
@using NeoUI.Blazor.Services
@using NeoUI.Icons.Lucide      @* optional but highly recommended — 1,640 icons *@
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chart components live in their own sub-namespace — add &lt;code&gt;@using NeoUI.Blazor.Charts&lt;/code&gt; to any file that uses them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Register services in &lt;code&gt;Program.cs&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NeoUI.Blazor.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NeoUI.Blazor.Primitives.Extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNeoUIPrimitives&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNeoUIComponents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Auto and WebAssembly projects, register in both the server and client &lt;code&gt;Program.cs&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Add CSS references to &lt;code&gt;App.razor&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Pre-built NeoUI styles — no Tailwind setup required --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"@Assets["&lt;/span&gt;&lt;span class="na"&gt;_content&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;NeoUI.Blazor&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;components.css&lt;/span&gt;&lt;span class="err"&gt;"]"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Pick one base color and one primary color --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"@Assets["&lt;/span&gt;&lt;span class="na"&gt;_content&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;NeoUI.Blazor&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;themes&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;zinc.css&lt;/span&gt;&lt;span class="err"&gt;"]"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"@Assets["&lt;/span&gt;&lt;span class="na"&gt;_content&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;NeoUI.Blazor&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;themes&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;blue.css&lt;/span&gt;&lt;span class="err"&gt;"]"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Prevents flash of unstyled content on page load --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"@Assets["&lt;/span&gt;&lt;span class="na"&gt;_content&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;NeoUI.Blazor&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;theme.js&lt;/span&gt;&lt;span class="err"&gt;"]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@Assets[...]&lt;/code&gt; 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 &lt;code&gt;ThemeSwitcher&lt;/code&gt; with all 85 runtime theme combinations, include all base and primary theme files (see the &lt;a href="https://neoui.io/docs/theming" rel="noopener noreferrer"&gt;Theming guide&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Add portal hosts to &lt;code&gt;MainLayout.razor&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Overlay and notification components render outside the component tree via a portal system. All four hosts belong at the end of your layout, after &lt;code&gt;@Body&lt;/code&gt; and outside any scrollable or clipping container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@inherits LayoutComponentBase

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen bg-background"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    @Body
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;ToastViewport&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;DialogHost&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ContainerPortalHost&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;OverlayPortalHost&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each host has a specific role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ToastViewport&lt;/code&gt; — renders toast notifications (position configurable)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DialogHost&lt;/code&gt; — required for programmatic &lt;code&gt;DialogService&lt;/code&gt; usage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ContainerPortalHost&lt;/code&gt; — inline overlays: Popover, Tooltip, DropdownMenu&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OverlayPortalHost&lt;/code&gt; — full-screen overlays: Dialog, Sheet, Drawer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Start building&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Button&amp;gt;&lt;/span&gt;Default&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Variant=&lt;/span&gt;&lt;span class="s"&gt;"ButtonVariant.Outline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Outline&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Variant=&lt;/span&gt;&lt;span class="s"&gt;"ButtonVariant.Secondary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Secondary&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;Dialog&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DialogTrigger&lt;/span&gt; &lt;span class="na"&gt;AsChild&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Variant=&lt;/span&gt;&lt;span class="s"&gt;"ButtonVariant.Outline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open Dialog&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/DialogTrigger&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DialogContent&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DialogHeader&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DialogTitle&amp;gt;&lt;/span&gt;Welcome to NeoUI&lt;span class="nt"&gt;&amp;lt;/DialogTitle&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DialogDescription&amp;gt;&lt;/span&gt;Beautiful Blazor components inspired by shadcn/ui.&lt;span class="nt"&gt;&amp;lt;/DialogDescription&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/DialogHeader&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;DialogFooter&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DialogClose&lt;/span&gt; &lt;span class="na"&gt;AsChild&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Variant=&lt;/span&gt;&lt;span class="s"&gt;"ButtonVariant.Outline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/DialogClose&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/DialogFooter&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/DialogContent&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Tailwind. No Node.js. No build tools. Just install and build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why now
&lt;/h2&gt;

&lt;p&gt;.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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built for agents and AI-assisted development
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;NeoUI ships a complete set of LLM-optimised docs at &lt;a href="https://neoui.io/llms.txt" rel="noopener noreferrer"&gt;neoui.io/llms.txt&lt;/a&gt; — 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;Getting&lt;/span&gt; &lt;span class="py"&gt;Started&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/getting-started.txt&lt;/span&gt;
&lt;span class="py"&gt;Installation&lt;/span&gt;      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/installation.txt&lt;/span&gt;
&lt;span class="py"&gt;Components&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/components.txt&lt;/span&gt;
&lt;span class="err"&gt;Full&lt;/span&gt; &lt;span class="py"&gt;Reference&lt;/span&gt;    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/components-full.txt&lt;/span&gt;
&lt;span class="py"&gt;DataGrid&lt;/span&gt;          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/datagrid.txt&lt;/span&gt;
&lt;span class="py"&gt;Theming&lt;/span&gt;           &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/theming.txt&lt;/span&gt;
&lt;span class="py"&gt;Architecture&lt;/span&gt;      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/architecture.txt&lt;/span&gt;
&lt;span class="py"&gt;Primitives&lt;/span&gt;        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/primitives.txt&lt;/span&gt;
&lt;span class="py"&gt;Templates&lt;/span&gt;         &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/templates.txt&lt;/span&gt;
&lt;span class="py"&gt;Patterns&lt;/span&gt;          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/patterns.txt&lt;/span&gt;
&lt;span class="py"&gt;Icons&lt;/span&gt;             &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/icons.txt&lt;/span&gt;
&lt;span class="py"&gt;Blocks&lt;/span&gt;            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/blocks.txt&lt;/span&gt;
&lt;span class="py"&gt;Changelog&lt;/span&gt;         &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://neoui.io/llms/changelog.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;patterns.txt&lt;/code&gt; file alone is worth bookmarking. It covers the real-world scenarios developers actually hit: validated forms, server-side DataGrid, programmatic &lt;code&gt;DialogService&lt;/code&gt;, runtime theme switching, &lt;code&gt;SidebarProvider&lt;/code&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;EditForm&lt;/span&gt; &lt;span class="na"&gt;Model=&lt;/span&gt;&lt;span class="s"&gt;"@_model"&lt;/span&gt; &lt;span class="na"&gt;OnValidSubmit=&lt;/span&gt;&lt;span class="s"&gt;"HandleSubmit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;DataAnnotationsValidator&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;FieldGroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Field&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;FieldLabel&lt;/span&gt; &lt;span class="na"&gt;For=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/FieldLabel&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;FieldContent&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Input&lt;/span&gt; &lt;span class="na"&gt;Id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;Type=&lt;/span&gt;&lt;span class="s"&gt;"InputType.Email"&lt;/span&gt;
                    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;bind-Value=&lt;/span&gt;&lt;span class="s"&gt;"_model.Email"&lt;/span&gt;
                    &lt;span class="na"&gt;Placeholder=&lt;/span&gt;&lt;span class="s"&gt;"you@example.com"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/FieldContent&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;ValidationMessage&lt;/span&gt; &lt;span class="na"&gt;For=&lt;/span&gt;&lt;span class="s"&gt;"() =&amp;gt; _model.Email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Field&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;Field&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;FieldLabel&lt;/span&gt; &lt;span class="na"&gt;For=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Password&lt;span class="nt"&gt;&amp;lt;/FieldLabel&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;FieldContent&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;Input&lt;/span&gt; &lt;span class="na"&gt;Id=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;Type=&lt;/span&gt;&lt;span class="s"&gt;"InputType.Password"&lt;/span&gt;
                    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;bind-Value=&lt;/span&gt;&lt;span class="s"&gt;"_model.Password"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/FieldContent&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;ValidationMessage&lt;/span&gt; &lt;span class="na"&gt;For=&lt;/span&gt;&lt;span class="s"&gt;"() =&amp;gt; _model.Password"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Field&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/FieldGroup&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Button&lt;/span&gt; &lt;span class="na"&gt;Type=&lt;/span&gt;&lt;span class="s"&gt;"ButtonType.Submit"&lt;/span&gt; &lt;span class="na"&gt;Class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sign in&lt;span class="nt"&gt;&amp;lt;/Button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/EditForm&amp;gt;&lt;/span&gt;

@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; } = "";
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;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 (&lt;code&gt;SidebarPillNav&lt;/code&gt;, &lt;code&gt;SidebarPillNavItem&lt;/code&gt;, &lt;code&gt;SidebarPillFade&lt;/code&gt;, &lt;code&gt;SidebarPillInset&lt;/code&gt;, &lt;code&gt;SidebarPillSpacer&lt;/code&gt;). All changes are additive and fully backward-compatible. The roadmap ahead includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Figma kit&lt;/strong&gt; — design tokens and component specs aligned to NeoUI's design system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More primitives&lt;/strong&gt; — expanding the headless layer with additional accessibility primitives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocks&lt;/strong&gt; — pre-built full-page sections and layout patterns (already in early preview at &lt;a href="https://neoui.io/blocks" rel="noopener noreferrer"&gt;neoui.io/blocks&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-assisted components&lt;/strong&gt; — exploring integration points for AI-powered form validation and content generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is open source, MIT licensed, and actively maintained. Contributions, issues, and feedback are all welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Website &amp;amp; docs: &lt;a href="https://neoui.io" rel="noopener noreferrer"&gt;neoui.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎮 Live demo: &lt;a href="https://demos.neoui.io" rel="noopener noreferrer"&gt;demos.neoui.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 NuGet: &lt;a href="https://www.nuget.org/packages/NeoUI.Blazor/" rel="noopener noreferrer"&gt;NeoUI.Blazor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 GitHub: &lt;a href="https://github.com/jimmyps/blazor-shadcn-ui" rel="noopener noreferrer"&gt;github.com/jimmyps/blazor-shadcn-ui&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 X: &lt;a href="https://x.com/neoui_io" rel="noopener noreferrer"&gt;@neoui_io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;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.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>shadcnui</category>
      <category>webcomponents</category>
    </item>
  </channel>
</rss>
