Best Practices

Patterns, tips, and answers to common questions for building production-ready applications with NeoUI.

Start with Styled Components

Always reach for NeoUI.Blazor styled components first. They have beautiful defaults, full theme support, and accessibility built in. Only drop to the Primitives layer when you need complete control over markup and styling — for example, when building a bespoke component that must match a custom design system that differs from shadcn/ui.

Use the AsChild Pattern for Triggers

When you want a styled Button to trigger an overlay, use AsChild on the trigger component. This lets you compose any button variant without wrapping it in an extra element or overriding default trigger styles:

RZ
@* Without AsChild — renders an extra default <button> wrapper *@
<DialogTrigger>Open</DialogTrigger>

@* With AsChild — your Button IS the trigger, no extra element *@
<DialogTrigger AsChild>
    <Button Variant="ButtonVariant.Destructive">Delete account</Button>
</DialogTrigger>

This pattern applies to DialogTrigger, PopoverTrigger, SheetTrigger, DropdownMenuTrigger, and all other trigger components.

Form Composition

Wrap related fields in FieldGroup — it spaces out multiple fields with consistent vertical rhythm. Wrap each individual control in Field to compose the label, control, help text, and validation message as a single accessible unit.

Always set For on FieldLabel to match the Id of the focusable input beneath it. This wires the HTML for attribute correctly so clicking the label focuses the right control.

Set ShowValidationError="true" on any input component to enable built-in EditForm validation integration. On submit, the component automatically applies error styling, the first invalid field is focused, and a validation tooltip with the error message appears inline — no separate FieldMessage required.

RZ
<EditForm Model="@_model" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />

    @* FieldGroup spaces multiple Field components with consistent vertical rhythm *@
    <FieldGroup>

        @* FieldLabel.For matches the Id of the focusable input below it *@
        <Field>
            <FieldLabel For="full-name">Full name</FieldLabel>
            <FieldControl>
                @* ShowValidationError: applies error style + auto-focuses first error + shows tooltip *@
                <Input Id="full-name" @bind-Value="_model.Name" ShowValidationError="true" />
            </FieldControl>
        </Field>

        <Field>
            <FieldLabel For="email">Email address</FieldLabel>
            <FieldControl>
                <Input Id="email" @bind-Value="_model.Email"
                       Type="InputType.Email" ShowValidationError="true" />
            </FieldControl>
            <FieldDescription>We'll never share your email.</FieldDescription>
        </Field>

        <Field>
            <FieldLabel For="role">Role</FieldLabel>
            <FieldControl>
                <Select Id="role" @bind-Value="_model.Role" ShowValidationError="true">
                    <SelectTrigger />
                    <SelectContent>
                        <SelectItem Value="admin">Admin</SelectItem>
                        <SelectItem Value="member">Member</SelectItem>
                    </SelectContent>
                </Select>
            </FieldControl>
        </Field>

        <Field>
            <FieldLabel For="budget">Budget</FieldLabel>
            <FieldControl>
                <NumericInput Id="budget" @bind-Value="_model.Budget"
                              Prefix="$" ShowValidationError="true" />
            </FieldControl>
        </Field>

        <Field>
            <FieldLabel For="start-date">Start date</FieldLabel>
            <FieldControl>
                <DatePicker Id="start-date" @bind-Value="_model.StartDate"
                            ShowValidationError="true" />
            </FieldControl>
        </Field>

        <Field>
            <FieldLabel For="notify">Email notifications</FieldLabel>
            <FieldControl>
                <Switch Id="notify" @bind-Value="_model.Notifications" />
            </FieldControl>
        </Field>

    </FieldGroup>

    <Button Type="ButtonType.Submit" Class="mt-2">Save</Button>
</EditForm>

Theme Management in Production

  • Ship only the theme files you use. If your app uses a fixed theme, include only one base and one primary CSS file. This avoids loading 22 theme files for users who cannot switch.
  • Always load theme CSS before components.css. NeoUI reads the CSS variables at component render time. If variables aren't defined yet the components fall back to browser defaults.
  • Include theme.js in <head>, not deferred. The script applies the saved theme class before paint to prevent a flash of unstyled content.
  • Use @Assets[...] for all static web asset references in App.razor. .NET 10 adds a fingerprint hash for cache busting on each deployment.

Don't Fight the Defaults

NeoUI components come with carefully considered defaults for accessibility, focus management, and keyboard behavior. Before adding custom JavaScript or overriding ARIA attributes, check whether the component already supports the behavior you need — it often does via a parameter. Overriding built-in accessibility handling is the most common source of regressions.

Use a Single Icon Library

All three icon libraries are included transitively with NeoUI.Blazor, but pick one and use it consistently throughout your application. Mixing Lucide, Heroicons, and Feather in the same UI creates visual inconsistency — different stroke weights, viewBox sizes, and design languages. NeoUI itself uses Lucide everywhere.

FAQ

NeoUI is an open-source Blazor component library that brings the shadcn/ui design system to .NET. It provides 100+ production-ready, fully accessible components with pre-built CSS, 85 theme combinations, and a headless primitives layer — distributed entirely as a NuGet package. No Node.js, no Tailwind setup, no build tooling required to get started.
Three principles drive every design decision. First, components should be beautiful by default and fully customizable — you should get a great result with zero configuration, but have complete escape hatches when you need them. Second, accessibility must be architectural: baked into the foundation (the Primitives layer) so the styled layer cannot accidentally regress it. Third, zero configuration means a developer should be able to install a NuGet package and immediately build — no understanding of build pipelines, CSS tooling, or JavaScript ecosystems required.
Blazor enables component-driven UIs with full C# type safety, eliminating an entire category of runtime errors that JS frameworks manage with TypeScript. State management, dependency injection, authentication, and form validation all work with familiar .NET patterns. For teams already invested in .NET, Blazor avoids the context switch of a separate JS build pipeline, package manager, and runtime. NeoUI exists specifically to give Blazor developers the same quality of design-system tooling that the React ecosystem has enjoyed for years.

Traditional Blazor component libraries have their own proprietary design language. Learning MudBlazor means learning MudBlazor's component API and theming system, which does not transfer to other ecosystems.

NeoUI is aligned with the shadcn/ui design system. This means: themes from ui.shadcn.com and tweakcn.com work directly; developers coming from React/shadcn/ui find the API immediately familiar; and the headless Primitives layer provides complete styling freedom. NeoUI also ships pre-built CSS in the NuGet package — there is no theme builder, runtime CSS generation, or Sass compilation step.

No. NeoUI is an independent implementation built from the ground up for Blazor and C#. It draws inspiration from shadcn/ui's design system and the Radix UI primitives pattern, but contains no code from either project. The two-layer architecture — headless C# primitives with CSS variable theming — is a novel approach for the Blazor ecosystem, not a transpilation or mechanical port.
No. components.css ships inside the NuGet package and covers all component styling. Tailwind is optional — useful if you want utility classes like bg-primary or text-muted-foreground in your own layouts. See the Theming guide for the optional Tailwind v4 integration.
Yes. All components work across every Blazor hosting model: Blazor Server, Blazor WebAssembly (standalone), and Blazor Auto. NeoUI is optimised for Auto mode (.NET 10) but makes no assumptions about the rendering environment. The targeted JS interop layer works correctly in all three modes.

The styled components and primitives work in Blazor Hybrid via BlazorWebView. The portal hosts render correctly inside the WebView's component tree. Some JS interop features (clipboard, keyboard shortcuts) may need platform-specific adjustments on certain MAUI targets.

Beyond Blazor Hybrid, a dedicated NeoUI for MAUI library — native controls styled with the same shadcn/ui design language — is already on our roadmap. Watch the repository for updates.

Yes — NeoUI's CSS variable system is 100% compatible. Copy any theme CSS from ui.shadcn.com/themes or tweakcn.com into your wwwroot/styles/theme.css and it works immediately. No conversion or mapping step is needed.
Component state is managed entirely through Blazor's standard cascading value and parameter system — nothing is stored in JS or in the DOM outside the Blazor tree. When Auto mode transitions from Server to WASM rendering, components reinitialize normally. Portal hosts are layout-level components, so they persist across the transition. The ThemeService persists the selected theme in localStorage so the theme is consistent across the transition.
Performance. ContainerPortalHost handles lightweight, frequently-toggled overlays (Popover, Tooltip, DropdownMenu). OverlayPortalHost handles heavyweight full-screen overlays (Dialog, Sheet, Drawer). Separating them means that hovering over tooltips or opening dropdowns does not trigger a re-render of the dialog host — and vice versa. Dialogs with complex content don't cascade re-renders into the rest of the overlay system.
The packages were renamed from BlazorUI.* to NeoUI.* in version 1.0.7. Update your NuGet references to the new package IDs and update _content/BlazorUI.Components/ paths in App.razor to _content/NeoUI.Blazor/. All component APIs are backward-compatible with 1.0.x.

Next Steps

Reconnecting...

Attempting to rejoin the server

Connection Lost

Retrying in seconds

Connection Failed

Failed to rejoin the server.
Please retry or reload the page.

Session Paused

The session has been paused by the server

Resume Failed

Failed to resume the session.
Please reload the page.