BlazorBlueprint.Primitives
4.0.1
dotnet add package BlazorBlueprint.Primitives --version 4.0.1
NuGet\Install-Package BlazorBlueprint.Primitives -Version 4.0.1
<PackageReference Include="BlazorBlueprint.Primitives" Version="4.0.1" />
<PackageVersion Include="BlazorBlueprint.Primitives" Version="4.0.1" />
<PackageReference Include="BlazorBlueprint.Primitives" />
paket add BlazorBlueprint.Primitives --version 4.0.1
#r "nuget: BlazorBlueprint.Primitives, 4.0.1"
#:package BlazorBlueprint.Primitives@4.0.1
#addin nuget:?package=BlazorBlueprint.Primitives&version=4.0.1
#tool nuget:?package=BlazorBlueprint.Primitives&version=4.0.1
BlazorBlueprint.Primitives
Headless, unstyled Blazor primitive components with ARIA attributes and keyboard support. Build your own component library using these composable primitives.
Features
- Headless & Unstyled: Complete control over styling — primitives provide behavior, accessibility, and state management without imposing any visual design
- Built with Accessibility in Mind: Includes ARIA attributes and keyboard interaction support
- Composition-Based: Flexible component composition patterns for building complex UIs
- Type-Safe: Full C# type safety with IntelliSense support
- State Management: Built-in controlled and uncontrolled state patterns
- Keyboard Support: Keyboard interaction support for interactive components
- Two-Layer Portal Architecture: Category-scoped portals (Container and Overlay) for efficient rendering
- .NET 10: Built for the latest .NET platform
Installation
dotnet add package BlazorBlueprint.Primitives
Setup
Register services in Program.cs:
builder.Services.AddBlazorBlueprintPrimitives();
Add the portal host to your root layout (MainLayout.razor):
<BbPortalHost />
Add a single import to _Imports.razor:
@using BlazorBlueprint.Primitives
Reference the primitives stylesheet from your host page (App.razor or index.html):
<link rel="stylesheet" href="_content/BlazorBlueprint.Primitives/css/primitives.css" />
It is small and carries no design decisions — only the rules the primitives' own markup depends on, such as hiding screen-reader-only announcements and sizing the dismiss overlay. Skip it and the Sortable list reads its keyboard instructions out as visible body text. Apps that also use BlazorBlueprint.Components can load it or not; the two stylesheets agree.
Available Primitives
| Primitive | Description |
|---|---|
| Accordion | Collapsible content sections with single or multiple item expansion |
| Alert Dialog | Modal requiring explicit acknowledgement, no dismiss via overlay or Escape |
| Checkbox | Binary selection control with indeterminate state and BbCheckboxIndicator sub-component |
| Collapsible | Expandable content area with trigger control |
| Context Menu | Right-click menu with keyboard navigation and positioning |
| Dashboard Grid | Widget layout state, drag-and-drop coordination, resize handling, responsive breakpoints |
| DataGrid | Headless data grid with sorting, filtering, pagination, selection, expansion, row grouping, and state management |
| Dialog | Modal dialogs with backdrop, focus management, and portal rendering |
| Dropdown Menu | Context menus with items, checkbox items, separators, and keyboard shortcuts |
| Hover Card | Rich preview cards on hover with delay control |
| Label | Accessible labels for form controls with automatic association |
| Popover | Floating panels for additional content with positioning |
| Progress | Accessible progress bar with determinate and indeterminate states |
| Radio Group | Mutually exclusive options with keyboard navigation |
| Scroll Area | Custom scrollbar with accessible ARIA scrollbar role and drag support |
| Select | Dropdown selection with cascading type inference and display text resolution |
| Separator | Semantic or decorative divider with orientation support |
| Sheet | Side panels that slide in from viewport edges |
| Slider | Range input with keyboard navigation and pointer drag support |
| Switch | Toggle control with BbSwitchThumb sub-component for automatic data-state sync |
| Table | Data table with header, body, rows, cells, and pagination |
| Tabs | Tabbed interface with keyboard navigation |
| Toggle | Pressed/active state with aria-pressed support |
| Tooltip | Brief informational popups with hover/focus triggers |
| Tree View | Hierarchical expand/collapse, selection, and checkbox state management |
Services
| Service | Description |
|---|---|
IPortalService |
Two-layer portal management with Container and Overlay categories |
IFocusManager |
Focus trapping and restoration for overlays |
IPositioningService |
Floating UI positioning with auto-update |
IKeyboardShortcutService |
Global keyboard shortcut registration and management |
DropdownManagerService |
Coordinates open/close state across multiple dropdowns |
API Reference
Accordion
<BbAccordion Type="AccordionType.Single" Collapsible="true" DefaultValue="item-1">
<BbAccordionItem Value="item-1">
<BbAccordionTrigger>Section 1</BbAccordionTrigger>
<BbAccordionContent>Content 1</BbAccordionContent>
</BbAccordionItem>
</BbAccordion>
| Parameter | Type | Default | Description |
|---|---|---|---|
Type |
AccordionType |
Single |
Single (one item open) or Multiple (many items open) |
Collapsible |
bool |
false |
When Single, allows closing all items |
Checkbox
<BbCheckbox @bind-Checked="isChecked" Indeterminate="@isIndeterminate">
<BbCheckboxIndicator />
</BbCheckbox>
| Parameter | Type | Default | Description |
|---|---|---|---|
Checked |
bool |
false |
Checked state |
Indeterminate |
bool |
false |
Shows partial/mixed state |
BbCheckboxIndicator renders the appropriate check or indeterminate SVG icon automatically based on parent state:
| Parameter | Type | Default | Description |
|---|---|---|---|
ChildContent |
RenderFragment? |
null |
Custom content instead of default icons |
Size |
int |
14 |
SVG icon size in pixels |
StrokeWidth |
int |
3 |
SVG stroke width |
Select
<BbSelect TValue="string" @bind-Value="selected" @bind-Open="isOpen">
<BbSelectTrigger>
<BbSelectValue Placeholder="Choose..." />
</BbSelectTrigger>
<BbSelectContent>
<BbSelectItem Value="@("a")" Text="Option A" />
<BbSelectItem Value="@("b")" Text="Option B" />
</BbSelectContent>
</BbSelect>
Select uses [CascadingTypeParameter] — child components infer TValue from the parent. Supports ItemClass for parent-level item styling.
| Parameter | Type | Default | Description |
|---|---|---|---|
Value |
TValue? |
— | Selected value (two-way bindable) |
Open |
bool |
false |
Open state (two-way bindable) |
ItemClass |
string? |
null |
CSS classes cascaded to all BbSelectItem children |
Dialog
<BbDialog @bind-Open="isOpen">
<BbDialogTrigger>Open</BbDialogTrigger>
<BbDialogPortal>
<BbDialogOverlay />
<BbDialogContent>
<BbDialogTitle>Title</BbDialogTitle>
<BbDialogDescription>Description</BbDialogDescription>
<BbDialogClose>Close</BbDialogClose>
</BbDialogContent>
</BbDialogPortal>
</BbDialog>
Sheet
<BbSheet>
<BbSheetTrigger>Open</BbSheetTrigger>
<BbSheetPortal>
<BbSheetOverlay />
<BbSheetContent Side="SheetSide.Right">
<BbSheetTitle>Title</BbSheetTitle>
<BbSheetDescription>Description</BbSheetDescription>
<BbSheetClose>Close</BbSheetClose>
</BbSheetContent>
</BbSheetPortal>
</BbSheet>
| Parameter | Type | Default | Description |
|---|---|---|---|
Side |
SheetSide |
Right |
Top, Right, Bottom, Left |
Popover
<BbPopover>
<BbPopoverTrigger>Open</BbPopoverTrigger>
<BbPopoverContent Side="PopoverSide.Bottom" Align="PopoverAlign.Center">
Content here
</BbPopoverContent>
</BbPopover>
| Parameter | Type | Default | Description |
|---|---|---|---|
Side |
PopoverSide |
Bottom |
Top, Right, Bottom, Left |
Align |
PopoverAlign |
Center |
Start, Center, End |
CloseOnEscape |
bool |
true |
Close when Escape key pressed |
CloseOnClickOutside |
bool |
true |
Close when clicking outside |
Tooltip
<BbTooltip DelayDuration="700" HideDelay="0">
<BbTooltipTrigger>Hover me</BbTooltipTrigger>
<BbTooltipContent>Tooltip text</BbTooltipContent>
</BbTooltip>
| Parameter | Type | Default | Description |
|---|---|---|---|
DelayDuration |
int |
700 |
Milliseconds before showing |
HideDelay |
int |
0 |
Milliseconds before hiding |
HoverCard
<BbHoverCard OpenDelay="700" CloseDelay="300">
<BbHoverCardTrigger>Hover for preview</BbHoverCardTrigger>
<BbHoverCardContent>Rich preview content</BbHoverCardContent>
</BbHoverCard>
| Parameter | Type | Default | Description |
|---|---|---|---|
OpenDelay |
int |
700 |
Milliseconds before showing |
CloseDelay |
int |
300 |
Milliseconds before hiding |
Dropdown Menu
<BbDropdownMenu ItemClass="px-2 py-1.5 cursor-pointer rounded hover:bg-accent">
<BbDropdownMenuTrigger>Menu</BbDropdownMenuTrigger>
<BbDropdownMenuContent>
<BbDropdownMenuItem>Cut</BbDropdownMenuItem>
<BbDropdownMenuItem>Copy</BbDropdownMenuItem>
<BbDropdownMenuItem Href="https://example.com" Target="_blank">Visit Site</BbDropdownMenuItem>
<BbDropdownMenuCheckboxItem @bind-Checked="isEnabled">Enable</BbDropdownMenuCheckboxItem>
</BbDropdownMenuContent>
</BbDropdownMenu>
| Parameter | Type | Default | Description |
|---|---|---|---|
ItemClass |
string? |
null |
CSS classes cascaded to all menu items |
BbDropdownMenuItem supports Href and Target for link items — renders as <a> when Href is set.
Switch
<BbSwitch @bind-Checked="isEnabled" class="relative h-6 w-11 rounded-full bg-input">
<BbSwitchThumb class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg" />
</BbSwitch>
BbSwitchThumb automatically syncs data-state ("checked" / "unchecked") from the parent via cascading parameter.
Radio Group
<BbRadioGroup TValue="string" @bind-Value="selected" ItemClass="flex items-center gap-2">
<BbRadioGroupItem Value="@("a")">Option A</BbRadioGroupItem>
<BbRadioGroupItem Value="@("b")">Option B</BbRadioGroupItem>
</BbRadioGroup>
| Parameter | Type | Default | Description |
|---|---|---|---|
ItemClass |
string? |
null |
CSS classes cascaded to all radio items |
Tabs
<BbTabs DefaultValue="tab1" Orientation="TabsOrientation.Horizontal"
ActivationMode="TabsActivationMode.Automatic">
<BbTabsList>
<BbTabsTrigger Value="tab1">Tab 1</BbTabsTrigger>
</BbTabsList>
<BbTabsContent Value="tab1">Content</BbTabsContent>
</BbTabs>
| Parameter | Type | Default | Description |
|---|---|---|---|
Orientation |
TabsOrientation |
Horizontal |
Horizontal, Vertical |
ActivationMode |
TabsActivationMode |
Automatic |
Automatic (on focus), Manual (on click) |
Table
<BbTable TData="Person">
<BbTableHeader>
<BbTableRow>
<BbTableHeaderCell>Name</BbTableHeaderCell>
<BbTableHeaderCell>Email</BbTableHeaderCell>
</BbTableRow>
</BbTableHeader>
<BbTableBody>
@foreach (var person in people)
{
<BbTableRow>
<BbTableCell>@person.Name</BbTableCell>
<BbTableCell>@person.Email</BbTableCell>
</BbTableRow>
}
</BbTableBody>
</BbTable>
| Parameter | Type | Default | Description |
|---|---|---|---|
SelectionMode |
SelectionMode |
None |
None, Single, Multiple |
SortDirection |
SortDirection |
None |
None, Ascending, Descending |
Portal Architecture
Primitives use a two-layer portal system for rendering overlay content:
- Container portals (
PortalCategory.Container): Dialog, Sheet — full-screen overlays - Overlay portals (
PortalCategory.Overlay): Popover, Select, Dropdown, Tooltip, HoverCard — positioned floating content
Each category has its own host (BbContainerPortalHost, BbOverlayPortalHost), so opening a tooltip doesn't cause Dialog portals to re-render. BbPortalHost is a convenience wrapper that renders both.
BbFloatingPortal keeps content mounted in the DOM when closed (ForceMount defaults to true), hidden via CSS. A data-state attribute ("open" / "closed") on the portal content enables CSS animations.
JavaScript Modules
Every primitive that needs JavaScript gets it from one bundle,
js/primitives/bb-primitives.js, loaded through PrimitiveModules:
var module = await PrimitiveModules.GetAsync(JSRuntime);
await module.InvokeVoidAsync("elementUtils.scrollIntoView", elementId, "nearest");
The identifier is namespace.function, where the namespace is the module's file name in
camelCase — clickOutside, elementUtils, positioning, focusTrap, and so on. The individual
files are still importable on their own if you need just one.
overlay.open is the one to reach for when adding a floating component: it positions, reveals,
keeps the element positioned and wires the dismissal listeners in a single call. Splitting that back
into separate awaits puts a network round trip between each step, on every open.
From a component, declare what you want rather than wiring it — BbFloatingPortal forwards it:
<BbFloatingPortal Dismiss="@(new FloatingDismissOptions {
ContentId = Context.ContentId,
TriggerId = Context.TriggerId,
OnOutsideInteraction = true,
OnEscapeKey = true })"
OnDismiss="@HandleDismiss">
The portal reports the gesture and does nothing else with it; what a dismissal means stays with the owner.
Two rules matter if you add a primitive that needs JavaScript:
- Re-export the new module from
bb-primitives.js. A module reached by its ownimport(...)from C# costs an extra circuit round trip on Blazor Server, every page load, cached or not. - Do not dispose what
GetAsyncreturns. It is shared by every component on the circuit. The returned reference ignores disposal so that a mistake here cannot break anything, but the call is still dead code.
Controlled vs Uncontrolled
All stateful primitives support both controlled and uncontrolled modes:
Uncontrolled (Component manages its own state)
<BbDialog>
<BbDialogTrigger>Open</BbDialogTrigger>
<BbDialogPortal>
<BbDialogOverlay />
<BbDialogContent>Content</BbDialogContent>
</BbDialogPortal>
</BbDialog>
Controlled (Parent component manages state)
<BbDialog @bind-Open="isDialogOpen">
<BbDialogTrigger>Open</BbDialogTrigger>
<BbDialogPortal>
<BbDialogOverlay />
<BbDialogContent>
<button @onclick="() => isDialogOpen = false">Close</button>
</BbDialogContent>
</BbDialogPortal>
</BbDialog>
@code {
private bool isDialogOpen = false;
}
Design Philosophy
BlazorBlueprint.Primitives follows the "headless component" pattern popularized by Radix UI and Headless UI:
- Separation of Concerns: Primitives handle behavior and accessibility; you handle the design
- Composability: Build complex components by composing simple primitives
- No Style Opinions: Zero CSS included — bring your own design system
- Accessibility by Default: ARIA attributes and keyboard navigation built-in
When to Use
Use BlazorBlueprint.Primitives when:
- Building a custom design system from scratch
- Need complete control over component styling
- Want to match a specific brand or design language
- Integrating with existing CSS frameworks or design tokens
Consider BlazorBlueprint.Components when:
- Want beautiful defaults with shadcn/ui design
- Prefer zero-configuration setup with pre-built CSS
- Need to ship quickly without custom styling
Documentation
For full documentation, examples, and API reference, visit:
License
Apache License 2.0 - see LICENSE for details.
The package includes LICENSE, NOTICE, and staticwebassets/THIRD-PARTY-NOTICES.txt. The bundled Floating UI and SortableJS assets retain their MIT licenses. These notices are also available at _content/BlazorBlueprint.Primitives/THIRD-PARTY-NOTICES.txt.
Contributing
Contributions are welcome! Please see our Contributing Guide.
| Product | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.12)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on BlazorBlueprint.Primitives:
| Package | Downloads |
|---|---|
|
BlazorBlueprint.Components
Pre-styled Blazor components built with shadcn/ui design and Tailwind CSS. Beautiful defaults that you can customize to match your brand. |
|
|
BlueprintShell
Embeddable Blazor shell built on BlazorBlueprint. Spin up a themed, dockable UI on a configurable port from any .NET application. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.0.1 | 81 | 9/19/2026 |
| 4.0.0 | 46 | 9/19/2026 |
| 4.0.0-beta.10 | 42 | 9/19/2026 |
| 4.0.0-beta.9 | 39 | 9/18/2026 |
| 4.0.0-beta.8 | 49 | 9/17/2026 |
| 4.0.0-beta.7 | 48 | 9/17/2026 |
| 4.0.0-beta.6 | 46 | 9/17/2026 |
| 4.0.0-beta.5 | 75 | 9/16/2026 |
| 4.0.0-beta.4 | 54 | 9/16/2026 |
| 4.0.0-beta.3 | 66 | 9/15/2026 |
| 4.0.0-beta.2 | 56 | 9/15/2026 |
| 4.0.0-beta.1 | 48 | 9/15/2026 |
| 3.17.0 | 947 | 9/13/2026 |
| 3.16.1 | 151 | 9/12/2026 |
| 3.16.0 | 3,812 | 9/4/2026 |
| 3.15.0 | 21,253 | 8/5/2026 |
| 3.14.1 | 17,995 | 7/16/2026 |
| 3.14.0 | 894 | 7/15/2026 |
| 3.13.0 | 11,791 | 7/2/2026 |
| 3.12.0 | 10,672 | 6/14/2026 |
## What's New in v4.0.0
### Breaking Changes
- **.NET 10**: the package now targets `net10.0` only and depends on `Microsoft.AspNetCore.Components.Web` 10.0.12. .NET 8 and .NET 9 are no longer supported.
- **BbPopoverContent**, **BbSelectContent**, **BbDropdownMenuContent**: the `JsOnClickOutside` JSInvokable method is removed, and **BbPopoverContent** also drops `JsOnEscapeKey`. Dismissal now arrives through the new `BbFloatingPortal.OnDismiss` callback.
- **BbTableRow** and **BbDataGridRow** no longer implement `IAsyncDisposable`. Code that awaited `DisposeAsync()` on these components must be updated.
- **BbFloatingPortal** opens and closes from `OnParametersSet` without awaiting interop. It no longer passes the floating element to JavaScript; the content is located by a `data-bb-portal` attribute instead.
- **BbFloatingPortal**: JavaScript now owns the resolved `data-side` attribute and a listbox's `data-focused` and `aria-activedescendant` state. Owners that rendered these from C# must stop, or the two writers will conflict.
- **click-outside.js**: the `onClickOutside` and `onEscapeKey` exports are removed. Use `BbFloatingPortal` with `FloatingDismissOptions` instead.
- **BbTableRow**, **BbDataGridRow**, **BbMenubarContent**, **BbSortable**: the Tailwind utilities these primitives render (row focus ring, menubar backdrop, sortable `sr-only` live region) are now `bb:`-prefixed. A consumer's own Tailwind build no longer emits them, so remove any `@source` that points at the library and update CSS or test selectors that matched the old class names.
- **BbSortable**: keyboard sorting is on by default, so each item, or its drag handle, is now a tab stop. Set `KeyboardSorting="false"` to keep the previous tab order.
- **BbSortable**: in a cross-list drop between two `BbSortable` lists, the source list's `OnRemove` now runs before the target list's `OnAdd`. Code that relied on the old order must be updated.
- **Portal hosts**: `BbPortalHost`, `BbOverlayPortalHost`, `BbContainerPortalHost` and `BbCategoryPortalHost` move from the `BlazorBlueprint.Primitives.Services` namespace to `BlazorBlueprint.Primitives`. Without the new `@using`, Razor emits a literal `<bbportalhost>` element with no build error and no portal host.
- **NavigationMenuContext**: `RegisterTrigger(ElementReference)` and `UpdateTriggerRef(int, ElementReference)` are replaced by `RegisterTrigger(object, ElementReference)`, `UnregisterTrigger(object)` and `TriggerIndexOf(object)`. A trigger is keyed by the component now, not by a position.
- **INativeOverlayService** gains `FallBackToJavaScript()`. A custom implementation of the interface must add it.
### New Components
- **BbDirectionProvider**: sets the writing direction for its content, so the library's layout mirrors for a right-to-left language. It renders no box of its own (`display: contents`), writes a `dir` attribute and cascades a `DirectionContext`.
- **BbMenuSub**, **BbMenuSubTrigger** and **BbMenuSubContent**: nested submenus for dropdown menus, context menus and menubars. The trigger opens on hover or click, and the arrow keys move into and back out of the submenu, including in right-to-left layouts.
- **BbMenuRadioGroup** and **BbMenuRadioItem**: generic single-choice items inside any menu, with `Value`, `ValueChanged` and `CloseOnSelect`.
- **BbContextMenuCheckboxItem**: a checkbox item for context menus, with `Checked`, `CheckedChanged` and `CloseOnSelect`.
### New Features
- **TextDirection** and **DirectionContext**: `TextDirection.Auto`, the default, follows `CultureInfo.CurrentCulture`, and `DirectionContext.Resolve` falls back to the culture when no provider is present, so a component used without one behaves as before.
- **Right-to-left keyboard navigation**: in **BbTabsList**, **BbToggleGroup**, **BbRadioGroup** and the tree, the horizontal arrow keys follow the reading direction while Up and Down keep their meaning. In a right-to-left tree, ArrowLeft opens a node.
- **BbSlider** mirrors in a right-to-left layout: the thumb is placed with `inset-inline-start`, the pointer's distance is measured from the reading edge, and ArrowLeft and ArrowRight swap.
- **Floating elements** copy the direction from their trigger as they open, through the same path that already carried a local theme across the portal boundary, so a provider works even when **BbPortalHost** sits outside it.
- **Native dialog rendering**: **BbDialog** gains a `RenderingStrategy` parameter. Set it to `OverlayRenderingStrategy.Native` to render a browser `<dialog>` element driven by `showModal()`, which works across Blazor render-mode boundaries and does not need a portal host.
- **OverlayRenderingOptions**: `AddBlazorBlueprintPrimitives` now accepts a configure callback to set a global `DefaultStrategy` for all overlays.
- **INativeOverlayService**: new scoped service that resolves the effective rendering strategy and drives the native `<dialog>` element (show, close, focus, and lifecycle events).
- **BbDialogContent** gains `CloseOnOverlayClick` to control whether a backdrop click closes a native dialog.
- **BbFloatingPortal** gains `Dismiss` (`FloatingDismissOptions`) and `OnDismiss` (`EventCallback<FloatingDismissReason>`), so the portal wires outside-click and Escape dismissal in the same call that opens the overlay.
- **BbFloatingPortal** gains `Keyboard` (`FloatingKeyboardOptions`). Listbox or menu keyboard handling is wired inside the open call, with `FloatingKeyboardKind` selecting the behaviour.
- **BbFloatingPortal** gains `SideElementId`, so JavaScript writes the resolved `data-side` attribute on a named element without a C# re-render.
- **BbFloatingPortal** gains `ScrollToCurrentIn` and `ScrollToCurrentSelector`, which scroll a chosen item into view before the overlay is revealed.
- **FloatingKeyboardOptions** gains `InitialFocus` (`"first"`, `"last"` or `"container"`), which moves focus into a menu after the overlay is revealed.
- **BbFloatingPortal** gains `AutoFocusId`, which focuses a named element one frame after the reveal, inside the call that opens the overlay.
- **BbFloatingPortal** gains `RestoreFocusToId` and `RestoreFocusOnClose`, so the browser returns focus to the trigger inside the close call for an intentional close only.
- **BbPopoverContent** gains `AutoFocusId`, `ScrollToSelected` and `ScrollToSelectedSelector`, so a popover-based list can focus its search box and open already scrolled to its current item.
- **DataGridEditMode** adds `Cell` (edit one cell in an isolated draft) and `Batch` (stage cell edits across rows until the batch is saved or discarded).
- **DataGridEditBuffer<TData>**: stages independently cloned rows as `DataGridEditChange<TData>` entries, keyed by a stable row key or by reference identity, without modifying the source records.
- **DataGridRowSnapshot<TData>.ApplyTo** copies captured property values onto another record. Unlike `Restore`, setter failures propagate.
- **bb-primitives.js**: all primitive JavaScript modules are bundled and re-exported under a namespace each (`focusTrap.createFocusTrap`, `positioning.computePosition`, etc.).
- **JsModules.GetAsync** and **PrimitiveModules.GetAsync**: shared, per-circuit module references that any component can use without owning or disposing them. **JsModules.TryGetLoaded** and **PrimitiveModules.TryGetLoaded** return an already-loaded module synchronously.
- **JsModules.Versioned** appends an assembly's informational version to a module path as a `v` query, so a new release is a new URL. **PrimitiveModules.ModuleUrl** exposes the versioned bundle URL.
- **elementUtils.observeNearBottom** and **observeHover**: new JavaScript observers that call .NET once when a list scrolls near its bottom or when the pointer moves onto a different item.
- **BbSortable keyboard sorting**: Space or Enter picks up an item, the arrow keys, Home and End move it, Space or Enter drops it, and Escape cancels. Ctrl plus Left or Right moves the item to the next connected list. `KeyboardInstructions` sets the accessible instructions linked to each handle.
- **BbSortable** gains `CanMove` and `CanDrop`, which reject a reorder or a cross-list drop before any list callback runs.
- **BbSortable** gains `DragOverlayTemplate`, a decorative preview that follows the pointer during a drag. Setting it turns on the fallback renderer.
- **BbSortable**: when `Handle` is not set, an element marked `data-bb-sortable-handle` inside an item becomes the drag handle. A disabled handle cannot start a drag.
- **BbSortable** gains `MovedAnnouncement`, `RemovedAnnouncement`, `ReceivedAnnouncement`, `MoveRejectedAnnouncement` and `DropRejectedAnnouncement`, so a localized app can translate the live-region text that was hard-coded English.
- **BbToggleGroup** gains `Required`, which stops the user from clearing the last selected value.
- **Theme scopes**: an overlay or a sortable drag preview opened from inside an element marked `data-bb-theme-scope` copies that element's theme CSS variables and font, and follows changes to them while open.
- **Tree keyboard navigation**: in a tree marked `data-tree-select="true"`, Space expands or collapses a branch without changing the value, and Enter selects the item or toggles its checkbox.
- **BbDialog**, **BbDialogPortal**, **BbPopover**, **BbHoverCard** and **BbSheet** accept `AdditionalAttributes`. These roots render no element of their own, so the attributes have nowhere to land and are reported once per component instance.
- **HoverCardContext** gains `CancelPendingClose()` and `PendingCloseCancelled`, so the trigger and the content can cancel each other's close timer.
- **INativeOverlayService.FallBackToJavaScript** stops resolving anything to the native strategy, so every later overlay renders through the JavaScript strategy.
### Bug Fixes
- **BbContextMenuContent**: a menu opened near the right or bottom edge of the viewport now flips back across the pointer, or clamps to the edge, instead of opening partly off-screen.
- **Overlays**: Escape now closes only the topmost open overlay. A popover inside a dialog no longer closes the dialog on the first press.
- **Overlays**: the close waits only for the overlay's own finite exit animation. A spinner or a child transition (tree chevron, hovered row, checkbox) no longer makes a closed popup reappear briefly.
- **Overlays**: navigating away from an open select, popover or menu no longer logs `System.ArgumentException: There is no tracked object`.
- **Overlays**: focus returns to the trigger on close even when a composed trigger supplies its own `id`.
- **BbFloatingPortal**: removed the fixed 500ms deadline on the portal host render signal, which timed out on slow connections. The wait is now unbounded and cancelled on close or disposal.
- **BbFloatingPortal**: a listbox is focused only after the reveal, so arrow keys no longer reach the trigger while the overlay is still hidden.
- **BbSelectContent**: hover and keyboard highlight are written by JavaScript only, so two options can no longer appear focused at once.
- **BbSelectTrigger**: opening the select with Enter or Space no longer closes it again on the native click that follows.
- **BbDropdownMenuContent**: clicking a nested portal inside an open menu no longer closes the menu. Outside-click detection now resolves elements by id per event, so it does not go stale after a re-render.
- **BbPopoverContent** and **BbDropdownMenuContent** no longer render a second time on open. The duplicate render raised a portal refresh mid-cycle that the host had to defer by a round trip.
- **BbDataGridRow**: controls inside an editing row now receive arrow keys and other key events, and row navigation shortcuts pause while the row is being edited.
- **BbDataGridRow**: pressing Enter on a button, picker trigger, checkbox or switch inside an editing row no longer also commits the row.
- **BbSlider**: right and middle clicks no longer start a drag, and a lost pointer capture now ends the drag.
- **elementUtils.focusElement** waits for the element to become visible before focusing it, so focus reaches portal content that is revealed after positioning, such as a nested calendar.
- **Tree view keyboard navigation** skips items marked `hidden` and keeps a tabbable item when filtering hides the previous tab stop.
- **Focus trap**: Tab no longer leaves a modal when focus is on the container or on a listbox outside the tab order, which WebKit allowed. With nothing focusable inside, Tab keeps focus on the container.
- **Menu keyboard navigation**: a parent menu no longer handles keys pressed inside a nested menu, and keys pressed with Ctrl, Alt or Meta are ignored. In a right-to-left menubar, Left and Right now move to the correct menu.
- **BbToggleGroup**: without a bound value, items now show the new pressed state as soon as they are toggled.
- **BbSortable** no longer calls `OnUpdate` for a move with an out-of-range or unchanged index, or when `Sort` is false.
- **JavaScript modules**: a browser or CDN that serves a stale copy of a bundled module no longer kills the circuit at the first call. The bundle fails at load with an error that names the file and says what to do.
- **NavigationMenuContext**: the close timer catches every exception, so an unexpected error in the fire-and-forget handler can no longer close the Blazor Server circuit.
- **Overlay roots**: extra HTML attributes splatted onto `BbDialog`, `BbPopover`, `BbHoverCard` or `BbSheet` no longer throw `InvalidOperationException`. A single `data-testid` on a styled wrapper crashed the render.
- **ARIA state attributes** bound to a bool rendered an empty value when true and vanished when false. `BbCollapsibleTrigger`, `BbCollapsibleContent`, `BbMenubarTrigger` and `BbTabsTrigger` now write `"true"` or `"false"`.
- **BbRadioGroup** suppressed the default action of every key, so Tab could not leave the group. Only the arrow keys are suppressed now.
- **BbSwitch** toggled twice for one Space press and ended where it started. The button's own click is the only handler now.
- **BbToggleGroupItem** in single mode announced as a radio with no state. It renders `aria-checked` with `role="radio"` and `aria-pressed` with `role="button"`, and the group root uses `role="radiogroup"` in single mode.
- **BbToggleGroupItem** never unregistered from its group, so arrow keys stepped onto buttons that had left the page. Arrow navigation now starts from the item that actually holds focus after a click or a Tab.
- **BbDropdownMenuTrigger** with `AsChild`: Enter or Space opened the menu and closed it again, because the trigger toggled and the child button's own click toggled back.
- **BbHoverCard**: moving the pointer from the trigger onto the card no longer closes it. The trigger's close timer is now cancelled as well as the content's.
- **BbHoverCardContent** wrote the enum name into `data-side` instead of the CSS value.
- **BbMenubarContent** and **BbDashboardWidget**: a consumer `style` no longer replaces the component's own. Any style at all left a closed menubar panel on screen and dropped a widget out of the grid.
- **BbNavigationMenuTrigger**: every trigger registered into the same slot, so arrow keys moved to the wrong button. Registration is keyed by the trigger and removed when it is disposed.
- **BbDialog** and **BbSheet** declared a `Dispose` method but not `IDisposable`, so Blazor never called it. Each root left a handler attached to its context.
- **BbPortalHost**: a live host reported itself missing and every portal logged the warning. Host registration is a clamped count, because two hosts overlap during a layout swap.
- **AddBlazorBlueprintPrimitives** configures an already registered `OverlayRenderingOptions` in place. Calling it before `AddBlazorBlueprintComponents` no longer loses the caller's configuration.
- **Native dialog**: a browser without `<dialog>.showModal()` now falls back to the JavaScript strategy. It used to log a warning and render a dialog with no backdrop, no focus trap and no Escape.
- **primitives.css** defines the few utility classes the primitives render themselves. In an app without the Components stylesheet, screen-reader-only text showed as ordinary paragraphs and the menubar dismiss overlay had no size to click.
### Improvements
- **Package licensing**: the NuGet package now includes `LICENSE`, `NOTICE` and `THIRD-PARTY-NOTICES.txt` (also served at `_content/BlazorBlueprint.Primitives/THIRD-PARTY-NOTICES.txt`), and the bundled Floating UI file carries its MIT license header.
- **README** documents the JavaScript bundle, the `overlay.open` pattern, the rules for adding a primitive that needs JavaScript, and the bundled license notices.
- **BbSortable** renders each item with `role="listitem"` and a `data-bb-sortable-item` attribute, and gives its live status region and keyboard instructions stable ids.
- **README** documents the `_content/BlazorBlueprint.Primitives/css/primitives.css` link and what breaks without it.
- **Missing portal host warning** now names the namespace move first, together with the `RZ10023` error that gives the mistake away.
### Performance
- **Overlays** open and close in one circuit round trip each. The portal registers content, positions, reveals, starts auto-update, and wires dismissal and keyboard listeners in a single interop call that is not awaited. On a 20ms round trip, a select open went from 11 messages to 1 and a close from 16 to 1 compared with v3.
- **Overlays**: arrow keys and option hover in a listbox no longer send a message to the server.
- **Overlays**: an element named by `AutoFocusId` is focused inside the open call, replacing a ready callback, a 50ms sleep, and a second round trip.
- **JavaScript modules** are imported once per circuit and shared, instead of once per component instance. Pages with many inputs issue far fewer round trips on Blazor Server.
- **Floating UI** is statically imported by `positioning.js`, removing a hidden dynamic import on the first position computation.
- **BbDataGridRow** and **BbTableRow** no longer attach keyboard and click handlers per row. **BbDataGrid** and **BbTable** delegate a single handler from the container, and rows opt in via `data-bb-row-keys` and `data-bb-row-click`.
- **BbSelectContent** scrolls the selected option into view and attaches the keyboard handler in the same call that opens the overlay, so the list appears already scrolled to the selection.
- **BbSelectContent**, **BbPopoverContent** and **BbDropdownMenuContent** restore focus to the trigger inside `overlay.close` instead of awaiting `FocusAsync` after the close render, saving a round trip on every Escape and every selection.
- **BbDropdownMenuContent** drops a redundant width-matching interop call; `MatchAnchorWidth` already covers it.
- **BbTooltipContent** and **BbHoverCardContent** no longer request the portal's ready callback, which was empty and cost a round trip on Blazor Server.
- **BbPopoverContent** requests the portal's ready callback only when a consumer has set `OnContentReady`.
- **BbSlider**: the drag preview stays in the browser through a `--bb-slider-position` CSS variable. Only the latest distinct snapped value is sent to .NET, at most every 50ms with one call in flight.
- **BbSlider** no longer raises `ValueChanged` or re-renders when the snapped value has not changed.
- **Scroll containers**: `observeNearBottom` checks the scroll position in the browser, coalesced to one check per frame, and calls .NET once on entering the near-bottom zone instead of once per scroll event.
- **Lists**: `observeHover` uses one delegated hover listener per list and reports only a genuine change of item, so pointer travel no longer sends a message per pixel.