BlazorHeadless 0.1.0

dotnet add package BlazorHeadless --version 0.1.0
                    
NuGet\Install-Package BlazorHeadless -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BlazorHeadless" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BlazorHeadless" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="BlazorHeadless" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BlazorHeadless --version 0.1.0
                    
#r "nuget: BlazorHeadless, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BlazorHeadless@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BlazorHeadless&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=BlazorHeadless&version=0.1.0
                    
Install as a Cake Tool

BlazorHeadless

A headless UI component library for Blazor: behaviour, accessibility, and state management without any visual opinion. Style it your way with plain CSS, Tailwind, or any design system.

Inspired by Headless UI, Radix UI, and Ark UI patterns, ported natively to Blazor.

Why headless?

  • Zero visual opinion — the library ships no CSS, only semantic HTML and data-* hooks.
  • Accessibility built in — ARIA roles, states, and keyboard interactions are handled for you.
  • Polymorphic rendering — every component renders as any HTML element via the As parameter.
  • Styling hooks via data attributes[data-state], [data-disabled], [data-active], and friends.
  • Attribute mergingclass and style concatenate; everything else lets the consumer win.
  • Controlled and uncontrolled — every stateful component supports both modes.
  • Render-prop context — child content receives a typed render context for state-driven UI.
  • Anchor positioning — automatic floating panel positioning with flip/shift via JS interop.

Requirements

  • .NET 10
  • Microsoft.AspNetCore.Components.Web 10.x

Setup

// Program.cs
builder.Services.AddBlazorHeadless();

This registers the JS interop service used by Dialog, Popover, Transition, Portal, FocusTrap, and anchor positioning.

If you use BhPortal, also add a portal outlet to your root layout:

<BhPortalOutlet />

Components

Component Description
BhMenu Dropdown menu with keyboard nav, typeahead, and virtual focus
BhListbox Custom select with single/multi-select, typeahead, and form integration
BhCombobox Typeable autocomplete with consumer-driven filtering
BhDialog Modal with focus trap, scroll lock, and inert background
BhPopover Non-modal floating panel with focus management and group coordination
BhDisclosure Single show/hide region
BhAccordion Single or multiple expandable sections
BhTabGroup Tabbed interface with keyboard navigation
BhSwitch Two-state toggle with optional hidden form input
BhCheckbox Custom checkbox with indeterminate state support
BhRadioGroup Single-select radio group
BhButton Polymorphic button with disabled and loading states
BhCloseButton Pre-wired button that closes the nearest Dialog, Popover, or Disclosure
BhField Form field grouping with BhLabel, BhDescription, BhInput, BhSelect, BhTextarea
BhFieldset Group form controls under a BhLegend, with cascading disabled state
BhTransition CSS class-based enter/leave animations with lifecycle callbacks
BhPortal Render children into a different part of the DOM tree
BhFocusTrap Trap keyboard focus inside a container
BhDataInteractive Forward data-hover / data-active / data-focus attributes for unified state styling

Anchor Positioning

Dropdown panels (BhMenuItems, BhListboxOptions, BhComboboxOptions, BhPopoverPanel) support automatic positioning relative to their trigger via the Anchor parameter:

<BhMenu>
    <BhMenuButton>Options ▾</BhMenuButton>
    <BhMenuItems Anchor="@(new BhAnchorOptions { To = "bottom start", Gap = 4 })">
        <BhMenuItem OnClick="Edit">Edit</BhMenuItem>
        <BhMenuItem OnClick="Delete">Delete</BhMenuItem>
    </BhMenuItems>
</BhMenu>

Placement options

Use top, right, bottom, or left to center along an edge. Combine with start or end for corner alignment:

top start    |  top     |  top end
left start   |  left    |  left end
right start  |  right   |  right end
bottom start |  bottom  |  bottom end

BhAnchorOptions

Property Default Description
To "bottom" Placement string
Gap 0 Space (px) between trigger and panel
Offset 0 Nudge along the alignment axis
Padding 8 Minimum space from viewport edges

Features

  • Auto-flip — flips to the opposite side when there's not enough space.
  • Auto-shift — clamps the panel within viewport bounds.
  • Auto-update — repositions on scroll, resize, and element size changes via ResizeObserver.
  • CSS variables — exposes --button-width, --anchor-gap, --anchor-offset, --anchor-padding on the panel.
  • Zero dependencies — self-contained positioning engine, no Floating UI or Popper needed.

Matching trigger width

.my-dropdown {
    width: var(--button-width);
}

Quick examples

<BhMenu>
    <BhMenuButton class="btn" Context="b">
        Options
        <span class="chevron @(b.IsOpen ? "open" : "")">▾</span>
    </BhMenuButton>
    <BhMenuItems class="dropdown">
        <BhMenuItem OnClick="Edit"   Label="Edit">Edit</BhMenuItem>
        <BhMenuItem OnClick="Delete" Label="Delete" Disabled="true">Delete</BhMenuItem>
    </BhMenuItems>
</BhMenu>

Listbox

<BhListbox TValue="string" Value="@person" OnValueChange="v => person = v">
    <BhListboxButton TValue="string" Context="b">
        @(b.Value ?? "Select…")
    </BhListboxButton>
    <BhListboxOptions TValue="string">
        <BhListboxOption TValue="string" Value="@("alice")">Alice</BhListboxOption>
        <BhListboxOption TValue="string" Value="@("bob")">Bob</BhListboxOption>
    </BhListboxOptions>
</BhListbox>

Combobox

<BhCombobox TValue="string" Value="@fruit" OnValueChange="v => fruit = v"
            OnQueryChange="Filter" DisplayValue="v => v ?? string.Empty">
    <BhComboboxInput TValue="string" Placeholder="Search…" />
    <BhComboboxOptions TValue="string">
        @foreach (var f in filtered)
        {
            <BhComboboxOption TValue="string" Value="@f">@f</BhComboboxOption>
        }
    </BhComboboxOptions>
</BhCombobox>

Popover

<BhPopover>
    <BhPopoverButton Context="b">Info ▾</BhPopoverButton>
    <BhPopoverPanel>
        <p>Panel content here.</p>
        <BhPopoverButton Context="closeBtn">Close</BhPopoverButton>
    </BhPopoverPanel>
</BhPopover>

Dialog

<BhDialog Open="@showDialog" OnClose="() => showDialog = false">
    <BhDialogBackdrop class="backdrop" />
    <BhDialogPanel class="dialog-panel">
        <BhDialogTitle>Confirm</BhDialogTitle>
        <BhDialogDescription>Are you sure?</BhDialogDescription>
        <BhCloseButton>OK</BhCloseButton>
    </BhDialogPanel>
</BhDialog>

Switch

<BhSwitch Checked="@enabled" OnCheckedChange="v => enabled = v" class="switch" Context="s">
    <span class="switch-thumb"
          data-state="@(s.IsChecked ? "checked" : "unchecked")"></span>
</BhSwitch>

Disclosure

<BhDisclosure>
    <BhDisclosureButton Context="d">@(d.IsOpen ? "Hide" : "Show") details</BhDisclosureButton>
    <BhDisclosurePanel>Hidden content here.</BhDisclosurePanel>
</BhDisclosure>

Accordion

<BhAccordion DefaultValue="item-1">
    <BhAccordionItem Value="item-1">
        <BhAccordionTrigger Context="t">
            Section 1 <span>@(t.IsOpen ? "−" : "+")</span>
        </BhAccordionTrigger>
        <BhAccordionContent>Content for section 1.</BhAccordionContent>
    </BhAccordionItem>
</BhAccordion>

Tabs

<BhTabGroup>
    <BhTabList>
        <BhTab>Account</BhTab>
        <BhTab>Profile</BhTab>
    </BhTabList>
    <BhTabPanels>
        <BhTabPanel>Account settings</BhTabPanel>
        <BhTabPanel>Profile settings</BhTabPanel>
    </BhTabPanels>
</BhTabGroup>

Common parameters

Every component inherits from BhComponentBase and supports:

Parameter Purpose
As Override the rendered HTML tag (e.g. As="a")
Id Explicit HTML id; auto-generated when omitted
Ref Action<ElementReference> for DOM access and focus management
AdditionalAttributes Captured unmatched attributes — class, style, data-*, aria-*, anything HTML

Styling with data attributes

Components emit data attributes that mirror their state:

[data-state="open"]          { /* expanded / open */ }
[data-state="closed"]        { /* collapsed / closed */ }
[data-state="checked"]       { /* switch/checkbox on */ }
[data-state="unchecked"]     { /* switch/checkbox off */ }
[data-state="indeterminate"] { /* checkbox mixed state */ }
[data-state="active"]        { /* selected tab */ }
[data-active]                { background: #eff6ff; }   /* highlighted menu/listbox option */
[data-selected]              { font-weight: 600; }      /* selected listbox/combobox option */
[data-disabled]              { opacity: 0.5; pointer-events: none; }
[data-loading]               { cursor: progress; }
[data-orientation="horizontal"] { /* tablists, radio groups */ }
[data-orientation="vertical"]   { /* tablists, radio groups */ }

Important CSS note

When your panel CSS sets an explicit display value (e.g. display: flex), you must add a [hidden] override so the hidden attribute works correctly:

.my-panel {
    display: flex;
    /* ... */
}

.my-panel[hidden] {
    display: none;
}

Tailwind users can register a single global rule once in their input file:

@layer utilities {
    [hidden] { display: none !important; }
}

Sample app

The repo ships with a sample project that demonstrates every component twice — once with hand-written CSS and once with Tailwind utilities, side by side. Both versions render the same headless components; only the styling layer changes.

src/BlazorHeadless.Samples/
├── Components/Pages/Css/        # Plain CSS demos     (routes: /<slug>)
├── Components/Pages/Tailwind/   # Tailwind v4 demos   (routes: /tw/<slug>)
├── tailwind/input.css           # Tailwind entry point + custom data-* variants
└── wwwroot/app.css              # Hand-written CSS for the CSS section

Run it with:

dotnet run --project src/BlazorHeadless.Samples

The Tailwind section uses Tailwind v4's standalone CLI, downloaded automatically on first build by an MSBuild target — no Node.js required. Custom data-attribute variants (data-state-open:, data-state-checked:, data-active:, data-disabled:, etc.) are registered in tailwind/input.css so you can style states the Tailwind way:

<button class="bg-white data-state-open:bg-zinc-100 data-disabled:opacity-50">…</button>

A header switcher on every demo page lets you flip between the CSS and Tailwind implementations of the same component.

Project status

Active development. APIs may evolve as the library approaches feature parity with Headless UI v2.

License

See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 89 5/24/2026

Initial preview release.