Changelog

Release history and version updates for Blazor Blueprint.

September 22, 2026

Fixed
  • The mobile DateRangePicker preset dropdown reflects the selected dates instead of defaulting to Today. Unmatched or partial selections show Custom; an empty selection shows Select date range.
  • Radar tooltips escape data-derived text, preventing HTML injection through labels or series names.
  • Signature stroke restoration waits for the drawn pad to mount and initialize, including repeated switches from typed mode.
  • Pivot parent headings and Gantt non-working-day bands redraw when their visible metadata changes. Initial Gantt build errors appear immediately.
  • ListBox, Slider and Checkbox keyboard navigation preserves Tab behavior; ListBox uses its visible label as its accessible name.
  • Chart click callbacks can be added, removed or replaced after initialization without remounting the chart.
  • Swipe cancellation discards queued movement updates. Explicitly setting FileUpload's bound Files to null clears the selection and releases removed file resources.

September 21, 2026

Added
  • Accessible names on picker, selection, OTP and upload controls; configurable
  • BbMapChart and BbMap: a world choropleth for visitors, sales, and other
  • AsChildDiagnostics.WarnIfUnconsumed<TTrigger> and AsChildTriggerDescription
  • BarcodeFormatException, in BlazorBlueprint.Primitives.Barcode. An ArgumentException, so
Fixed
  • Responsive tab adding/reordering, conditional step order, required-chip dismissal,
  • Date/time restrictions now also apply to Now and empty-value time stepping.
  • Primitive menus now support the documented keyboard and controlled-state behavior.
  • Tinted bubble contrast, message alignment, editable-date invalid semantics,
  • ScrollToTop observes changing options and late targets, preserves keyboard focus,
  • Upload text/errors, AM/PM labels and MultiSelect removal/count labels are localizable.
  • BbDrawer reports OpenChanged when it is left to itself. Without Open bound, the drawer
  • A trigger in AsChild mode now says so when nothing inside it can use it. Eleven triggers
  • OnBuilt no longer rebuilds forever when the page that declares the chart handles it.
  • BbGantt no longer wires up JavaScript against an element that is not there. The chart is
  • A Gantt's tree column shows the task name again when the chart declares its own Columns.
  • A Gantt's column resize handles are wired on the first mount. They were drawn but never
  • BbBarcode no longer prints Arg_ParamName_Name instead of an error on WebAssembly. It

September 20, 2026

Added
  • **BbGantt, a plan drawn against a timeline: a task list down the side, a bar for every task
  • **BbPivotDataGrid, a cross-tabulation: one field down the side, another across the top, and a
  • BbBarcode, fourteen linear symbologies drawn as SVG, on encoders written from scratch in C#.
  • BbQrCode, a scannable code drawn as SVG, on a QR encoder written from scratch in C#. No
  • BbPickList, two lists and the buttons that move options between them. Pick rows and press
  • BbListBox, an always-visible list of options. No trigger, no popover: the choices stay on
  • BbSignature, a signing field, on the new BbSignaturePad primitive. Sign by drawing or by
  • BbSwipeArea, a headless swipe gesture primitive. Wrap any content and hear about swipes
Changed
  • BbTabsList can now add, close, rename and reorder its tabs. Four flags — Addable,
  • datagrid-columns.js is now table-columns.js, shared by BbDataGrid and BbGantt. It was

September 19, 2026

Added
  • The last two chart gaps: a rose and a sankey.
  • Five new components for links, search highlighting, image fallbacks, scroll-to-top navigation, and unsaved-change prompts.
  • Right-to-left support. BbDirectionProvider sets the writing direction for everything inside it, so the library mirrors for Arabic or Hebrew. Wrap the layout in it, including the overlay hosts. It renders no box of its own — display: contents — so adding it changes no layout; it writes a dir attribute and cascades a DirectionContext. TextDirection.Auto, the default, follows CultureInfo.CurrentCulture, which is what every component did before the provider existed, so nothing changes until you ask for it.
  • BbChip and BbChipSet. A chip is the interactive counterpart to a badge: it carries a selected state, a dismiss button, or both. Five variants and three sizes, each variant with its own selected treatment, plus an Icon fragment that takes an avatar as readily as an icon. A chip renders as a plain span until something makes it interactive, so a static chip is not announced as a button; a selectable one carries aria-pressed, and the dismiss button gets its own accessible name from DismissLabel or the localized Chip.Dismiss.
  • BbFab. A floating action button: raised, pinned to a corner, carrying the one main action of a screen. It is built on BbButton, so it works as an AsChild trigger — that is how a speed dial is composed, with the button opening a BbDropdownMenu instead of running an action. Five placements written in logical properties, so the corner follows the reading direction, pinned to the viewport or to the nearest positioned ancestor through Fixed. It sits above a bottom navigation bar and below the overlay layer, and clears the device safe area so a home indicator cannot cover it. Supplying ChildContent widens the button with a label. Corners follow the theme's radius by default — the same rounded-md every other component uses — and Shape="FabShape.Circle" makes it a full circle instead, or a pill once it carries a label. An icon-only button needs AriaLabel — it has no text of its own.
  • BbStepper and BbStep. A progress indicator for a sequence of steps, horizontal or vertical, with optional per-step content rendered only while that step is active. It is deliberately not a form: it holds no EditContext, validates nothing and renders no navigation buttons, which is what separates it from BbFormWizard. State is derived from position — behind the active step is complete, ahead is pending — and BbStep.State overrides that, which is how an error or a skipped step is shown. Clickable turns each marker into a button, and a Disabled step refuses activation. The indicator is an ordered list, the active step carries aria-current="step", and each step adds hidden text with its position and its state, so a state is never colour alone.
  • Active hours. ActiveHours names the part of each weekday the schedule is about, and everything outside it is muted. A day with no range is muted end to end, so weekends need no entry; two ranges for one day mute the gap between them, which is how you get a lunch break. Times are wall times in the display zone, so a day that gains or loses an hour keeps the same clock boundaries. Start is inclusive and End exclusive; TimeOnly cannot express 24:00, so TimeOnly.MinValue as the End runs to the end of the day. Muted slots say so to a screen reader rather than relying on the tint. An empty list — the default — mutes nothing.
  • Name your own time zones. The editor listed every IANA zone the host supports — roughly 600 of them, labelled by identifier, so Australia/Sydney read as "Australia/Sydney". TimeZones takes a list of SchedulerTimeZone(Id, Title) and shows only those, under your names. IDs are validated when the parameter is set, so you cannot rename your way into a zone that does not exist. An event already stored in a zone you did not list is still offered, so opening it cannot quietly move it to whichever zone happened to sort first. Leaving TimeZones empty keeps the old full list.
  • Your own fields in the event editor. SchedulerEvent is no longer sealed: derive from it to carry a description, an agenda, a customer ID. Clone() is virtual and a protected CopyTo() handles every field the base type owns, so an override only writes its own. NewEventFactory decides the type of a newly created event — without it a new event is the base type and your fields have nowhere to live. EditorContent then renders your fields in the editor; render @context.DefaultContent to keep the built-in ones and add around them, or leave it out to replace them. The context carries Draft, IsNew, Scope and DefaultContent.
  • BbScheduler gains a Month view. SchedulerView.Month renders six week rows — always six, so the grid height does not change between months — with multi-day events drawn as bars across the days they cover and single-day events as chips. Previous and Next step a calendar month rather than six weeks. MaxEventsPerDay caps each cell; a bar spends that budget in every day it covers, so a day carrying one shows fewer chips and counts the difference into its own "+x more". Resources act as a filter here rather than lanes, because a month cell cannot carry a column per resource. AllowDrag and AllowResize are ignored in Month: the pointer code derives times from the time grid's fixed 40px-per-slot lane geometry, which a month cell does not have.
  • All-day events. Set SchedulerEvent.IsAllDay and the event draws as a bar in a band above the time grid instead of a block inside it. The band does not scroll with the day, and its columns are pinned to the lane columns below. Start and End are then read as dates in the event's TimeZoneId and their time components are ignored, so flipping the flag on existing timed data is safe. End is exclusive — a single all-day event on the 14th runs from the 14th to the 15th — which matches iCalendar's DTEND and keeps the existing half-open range checks unchanged; the editor asks for the last day the event covers and converts.
  • Multi-day all-day events are one bar, not a chip per day. Runs that overlap stack into rows, longest at the top; runs that never touch share a row. A run leaving the visible range is squared off at the join, and both halves carry the whole event's dates in their aria-label rather than their own segment's. With two or more resources visible the band groups by resource — one row per resource, one column per day — rather than mirroring the day-major lane order below it: lanes run Mon/Room A, Mon/Room B, Tue/Room A, so one room's two-day event occupies non-adjacent columns and no single bar could span them. The band then stops lining up column-for-column with the grid, which is the price of drawing the bar correctly. Narrowing VisibleResourceIds to one resource restores the alignment.
  • Choose which resources to show. VisibleResourceIds is a two-way bindable list of resource IDs: null shows every resource, an empty list shows none and says so, and the empty string selects the Unassigned lane. ShowResourceFilter draws a BbMultiSelect in the toolbar bound to the same property, so a built-in picker and an application's own UI cannot disagree. Off by default, so existing schedules are unchanged.
  • Context menus on slots and events. Right-click an empty slot for New event; right-click an appointment for Edit event and Delete event. Delete confirms without opening the editor first. Both menus are replaceable through SlotContextMenuContent and EventContextMenuContent: render the context's DefaultItems to keep the built-in entries and add your own around them, or leave it out to replace the menu entirely. The slot context carries Start, End and Resource; the event context carries the SchedulerOccurrence.
  • BbScheduler.ToolbarContent. The toolbar is rendered inside the component, so there was no way to restyle or rearrange it. Render @context.DefaultContent to keep the built-in toolbar and add around it, or leave it out to replace it. The context carries read state plus the three commands that cannot be expressed from outside: Navigate knows the step size for the current view (one day, seven days, one month), and GoToToday resolves today in the schedule's time zone rather than the server's. Date, View, FirstDayOfWeek and VisibleResourceIds stay ordinary bindable parameters.
Changed
  • Quill 2.0.3 now ships inside the package, so BbRichTextEditor needs no setup. The host page no longer needs a <script> and a <link> for Quill — until this change, forgetting them left the editor dead with nothing but a console error to say why, which was the one component in the library that did not just work on install. The interop loads lib/quill/quill.js and lib/quill/quill.core.css from the package's own static assets on first use, with the same lazy single-flight shape the ECharts renderer already uses: nothing is fetched on a page with no editor, and fourteen editors on one page still fetch once. Quill's dist build is UMD rather than ESM, so it is loaded through a <script> tag rather than a dynamic import(), which is the only way the two loaders differ. A host that wants a different Quill build keeps full control: window.Quill is checked first, so loading your own copy before the first editor renders leaves the bundled one untouched. The stylesheet is injected unlayered, exactly where a host's own <link> put it, so the themed overrides in blazorblueprint.css — which are !important inside @layer components — still win as before. Quill's BSD 3-Clause notice is in THIRD-PARTY-NOTICES.txt alongside the ECharts and Tailwind ones. This adds roughly 214 KB of static assets to the package.
  • Clicking a scheduler event highlights it instead of opening the editor. Double-click it, press Enter while it is focused, or use the context menu's Edit event. This matches what an empty slot already does, and it stops a stray click on the way to a drag throwing the editor in your face. Highlighting is exclusive: selecting an event clears a highlighted slot or day, and vice versa. Two occurrences of one series highlight independently.
  • Month view creates and edits like the other views. Double-click anywhere in a day cell — not only the day number — to open the editor for a new event at StartHour. Double-click an event to edit it. Right-click empty space for New event, or an event for Edit and Delete.
  • Clicking an empty scheduler slot highlights it instead of opening the editor. Double-click it, or press Enter while it is focused, to create an event; Space highlights without creating. Enter created an event before this change and still does, so keyboard users lose nothing. The highlight is exactly one slot tall, is keyed to the lane as well as the time so the same hour in two resource lanes stays distinct, and clears on navigation, a view change or a resource-filter change. A drag that finishes over an empty slot creates nothing.
Added
  • A themed reconnection dialog for Blazor Server. Give the host page an element with
Fixed
  • dark: utilities followed the operating system instead of the dark-mode toggle. Dark mode is
  • The radial chart's centre label sat off-centre, and its chart title vanished. The label in the
  • Radial bar labels no longer overwrite each other. ShowLabels placed each label at the angular
  • DataGrid cell editors no longer cover Save and Cancel, or wreck a checkbox. The editor slot reset every direct-child button, which was wrong in both directions. A combobox and a multi select wrap their trigger in a container, so the reset never reached it and the trigger kept its own default width — 200px and 300px — inside a narrower cell, overflowing the slot and painting straight over the addon holding Save and Cancel. Both buttons stayed in the DOM, visible and reachable by keyboard, so nothing short of a hit test showed the problem. Meanwhile a checkbox, switch or toggle editor is a plain button, so the same reset stretched it to the full cell and stripped its border, leaving an unchecked cell looking empty.
  • Month view no longer claims no resources are selected when one is. The empty state inferred emptiness from the lane count, and Month builds no lanes at all, so filtering to a single resource and switching to Month replaced the grid with "No resources are selected". It now asks the filter directly.
  • The scheduler's date heading no longer jumps above the navigation on a narrow container. The heading carried an ungated row-start-1 while its neighbouring col-start-2 was gated at the @4xl container breakpoint (896px). Below that width the toolbar grid collapses to one column, and grid places explicitly positioned items first, so the heading claimed row 1 and pushed the previous/today/next buttons and the view controls into rows 2 and 3. Reported from the documentation site.
  • The scheduler's time-zone label no longer reads as part of the view button group. It shared a gap-2 flex row with the week-start select and the Day/Week/Work week buttons, so the 8px beside it was the same 8px that separates the buttons from each other. The three view buttons now form their own group, which also stops them splitting across a wrap boundary mid-group.
  • A context menu opened near a viewport edge no longer falls off screen. BbContextMenuContent positioned itself at the raw pointer coordinates and never measured against the viewport. It now flips back across the pointer when it does not fit, the way a native context menu does, and clamps when it fits on neither side. Not new, and not specific to the scheduler, but a tall scrolling scheduler is where it showed up.
  • TagInputTrigger.Blur. BbTagInput could only commit a tag on a keystroke — Enter, comma, space, tab or semicolon — so text the user had typed but not confirmed was thrown away when they clicked elsewhere. Add Blur to AddTrigger and leaving the input commits it. It is the only trigger that is not a key, and it is off by default, so nothing changes unless you ask for it.
Changed
  • A multi-day event in BbEventCalendar is one bar, not a chip in every day it covers. An event running Thursday to Saturday drew three identical chips, so it read as three separate events that happened to share a title — the other half of #544. It is now a single bar across the days it covers, in both the month and the week view. Runs that overlap stack into lanes, longest at the top; runs that never touch share a line. An event crossing a week boundary becomes one bar per row, squared off at the join so the two halves read as one event, and both halves carry the whole event's dates in their aria-label rather than their own segment's. Bars are positioned with a calc() over the seven-column grid, so they stay on the column boundaries at any width with no measuring and no JavaScript.
Documentation
  • Running your own Tailwind build needs one rule in your own stylesheet: the default border colour. Tailwind's preflight sets border: 0 solid, and that shorthand resets border-color to currentColor. Both stylesheets write their preflight into the shared base layer at the same specificity, so whichever loads last wins — and that is the application's. Without the rule an application's own border, border-b and so on render in the text colour rather than the theme's grey; library components are unaffected, because they carry their own colour on a bb:-prefixed utility. Not new in v4 — the default has sat in @layer base since at least 3.17.0 — but v4 is the release that makes running your own Tailwind build alongside the library safe, so more people do it, and upgrading is when you rebuild your CSS and notice. Documented in the v4 migration guide and THEMING.md, and the migration checklist no longer claims there is nothing to do.
Fixed
  • The calendar's day grid is centred instead of hanging to the left. The seven day cells are a fixed w-9 each — 252px — while the table is as wide as the month and year selects above it, which is wider. The rows were left-aligned, so the difference showed as dead space to the right of the dates. It appeared when the year select was widened from 80px to 100px (b39be911) to stop the year truncating, which is why it looked like it came from nowhere. The header row and every week row now centre their cells, so the day names stay over their columns and the space falls evenly on both sides. BbDatePicker, BbDateRangePicker and BbDateTimePicker embed the same calendar and pick the fix up with it.

September 18, 2026

Added — Component expansion
  • DateInput and TimeInput with culture-aware segments, keyboard navigation/increments, nullable drafts, inclusive bounds, read-only/disabled behavior, optional pickers and EditContext validation.
  • Sortable keyboard interaction and menu composition. Sortable supports pickup/reorder/drop/cancel, connected-list transfer with Control+Left/Right, typed move/drop predicates, a reusable handle and custom inert drag previews. DropdownMenu, ContextMenu and Menubar gain shared submenu and radio-item families; ContextMenu also gains checkbox items.
  • Theme presets and scoped appearance. Density, font family, surface and menu options persist with existing colors and radius. ThemeScope carries local settings into floating overlays; ThemeSwitcher can expose design settings. Applications supply font assets.
  • DataView selection, grouping and list virtualization. Stable keys preserve selections after item reloads; group headings remain mounted. Virtualization covers loaded list rows, while provider fetching stays page/batch based. MobileToolbar places sorting and FilterContent in a bottom sheet. Local data is no longer truncated when pagination is hidden.
  • Practical form controls. MultiSelect adds FooterContent and CloseAsync. FilterBuilder adds saved presets, searchable fields and per-field value editor templates. Select gains a bottom-sheet presentation with shared option behavior and near-bottom loading.
  • Mobile components. AppBar, BottomNav/BottomNavItem, NotificationBadge, QuantityStepper and SectionHeader, plus Drawer viewport snap points with pointer/keyboard resizing and optional drag dismissal.
  • Reusable motion. Motion presets/custom keyframes and viewport/hover/press/manual triggers; HeightAnimation for expansion and resizing; SelectionIndicator; PageTransition, ScreenTransition and RenderStateProvider. Animations honor reduced motion, cancel stale work, and retain usable prerendered content. Screen/page transitions animate incoming content; they do not retain outgoing routes.
  • Carousel and navigation. Carousel adds autoplay with pause/resume, drag navigation, measured responsive slide bounds, visible slide counts, gaps, indicators and index/change callbacks. Sidebar gains a Pill collapsed mode, compact navigation, inset spacing and a shared selection indicator.
  • Small controls. Semantic/soft Badge variants and BadgeIcon, ToggleGroup Required/Scrollable options, and solid/dashed/dotted Separator patterns. Required toggle groups retain the last selection; applications supply the initial value.
  • Discoverable component inventory. Document all 42 new styled components and six supporting headless menu components in README. The component homepage, sidebar and command search share the same grouped demo-page catalog, with one destination per actual demo. Standalone mobile and motion components have focused pages; composition helpers stay in their owning component's examples and API references. Sidebar and homepage badges identify new components only; API-reference badges mark specific v4 additions against the v3.17 release.
  • Permanent live examples, copyable snippets and API reference entries accompany each addition.
  • Browser regression coverage. Add a Playwright suite for Server, WebAssembly and Interactive Auto in Chromium and WebKit, covering keyboard navigation, mobile overlays, scoped themes, scheduler editing and Auto's transition to WebAssembly.
Added
  • DataGrid cell and batch editing. DataGridEditMode.Cell saves each accepted cell independently; DataGridEditMode.Batch stages changes until the batch is committed. Drafts are isolated through EditItemFactory, validated before saving, and retained when persistence rejects a change. Applications provide deep-copy factories and atomic batch persistence. Editors support keyboard save/cancel and custom text, numeric, date, select and checkbox controls; accepted changes reapply the active sorting.
  • BbScheduler for time-slot scheduling. Day, Week and WorkWeek views include resource lanes, overlapping appointments, a Bb component event editor, and delete confirmation. Week view offers a bindable Monday/Sunday start through FirstDayOfWeek/FirstDayOfWeekChanged. WorkWeek always shows Monday–Friday, navigates in seven-day steps, and preserves the full-week start preference.
  • Scheduler drag, resize and slot sizes. Drag events to move them between times and resource lanes, or extend the top/bottom borders to change start/end times. Gestures snap to SlotMinutes, including 15, 30 and 60 minutes, and use the same persistence/rejection callbacks as editor saves. AllowDrag, AllowResize and ReadOnly control interaction.
  • Scheduler recurrence and time zones. Daily, weekly, monthly and yearly rules support editing one occurrence or an entire series, with independent event/display IANA time zones and validation of skipped or repeated daylight-saving times. EnableTimeZones="false" hides zone controls and uses TimeZoneId for display and editing while preserving stored instants and recurrence zones.
  • Form field wrappers for the five new bindable controls. BbFormFieldDateInput, BbFormFieldTimeInput, BbFormFieldTreeSelect, BbFormFieldCascader and BbFormFieldQuantityStepper complete the set: every control in the library that exposes a ValueExpression now has a wrapper that supplies the label, helper text and validation message. Each was already built for EditContext; only the surrounding field was missing. BbFormFieldTreeSelect reads ValuesExpression in Multiple mode and ValueExpression otherwise, so the validation message follows whichever one you bind. BbCascader gains the Required parameter BbTreeSelect already had, and BbCascader, BbTreeSelect and BbQuantityStepper gain AriaDescribedBy — without it a wrapper had no way to point the control at its own error text.
  • BbTreeSelect and BbCascader. Searchable hierarchy pickers with stable keys, form bindings, clear/disabled states and leaf-only selection. TreeSelect supports single/multiple selection with cascading parent checkboxes and indeterminate states. Cascader provides column navigation, full-path search, optional branch selection and keyboard/RTL navigation; opening a selected path or expanding a branch reveals the newest column.
  • FileUpload transport lifecycle. Optional UploadHandler callbacks receive a size-limited browser stream and cancellation token, report progress, and support cancellation and retry. Browser file references survive subsequent selections; retry starts a fresh attempt from zero. Applications supply the upload destination.
  • BbEventCalendar.ContainerClass. Styles the view container — the month grid, the week grid or the agenda list — rather than the root, which also wraps the toolbar and so could not be used to size the calendar itself. The same classes apply in every view, so a height survives a switch between Month, Week and Agenda. Pair Class="flex h-full flex-col" with ContainerClass="grow overflow-auto" to fill the space a page gives you. Requested in #544.
  • BbDataView.SearchDebounceMs. The toolbar search now waits out a typing pause before it filters, with the same 300 ms default as BbDataGrid (#543).
Fixed
  • Card and other borders no longer render near-black in an application that runs its own Tailwind build. The default border colour was a bare * { border-color: var(--border) } in the shared base cascade layer. A consumer's own Tailwind writes its preflight into that same layer, and preflight's border: 0 solid resets the colour to currentColor — two * selectors, one layer, identical specificity, so whichever stylesheet the page links second won. Link blazorblueprint.css first, which is what the prefixed-utility fix for #496 encourages, and every library border turned the foreground colour. The rule now matches on the class attribute instead, which settles it on specificity rather than link order, and stays in base so a consumer's own border-* utilities still win. Scoped to the library's own prefixed classes, so it never touches consumer markup (#527).
  • BbDataGrid global search honours SearchDebounceMs again. The search field was left on BbInput's UpdateTiming.OnChange default, which reports only on blur or Enter — so the grid's own Task.Delay debounce never started, an ItemsProvider was not called while typing, and clearing the box did not reload until the field lost focus. The input now debounces in the browser at SearchDebounceMs, so a run of keystrokes costs one provider call rather than one per key, which matters most on Blazor Server. BbDataView's toolbar search had the same defect and takes the same fix (#543).
  • A live <BbPortalHost /> no longer reports itself missing. Registration was a boolean, and two hosts overlap more often than that allows: BbPortalHost is itself two category hosts, and a layout swap initialises the incoming page's host before the outgoing layout's host disposes. Whichever disposed first cleared the flag for the host still rendering. It is a count now, clamped so an unbalanced dispose cannot poison a later registration (#545).
  • Extra HTML attributes no longer crash a render. BbDialog, BbSheet, BbPopover, BbHoverCard, BbAlertDialog, BbAlertDialogPortal and BbDataGridColumnVisibility forwarded captured attributes to a component that could not accept them, so a single data-testid threw InvalidOperationException. BbColorPicker, BbDateRangePicker, BbTimePicker, BbThemeSwitcher and BbResponsiveNavContent now render those attributes on their trigger instead of losing them; the context-only roots accept them and log once that they have nowhere to go.
  • Attributes that were accepted and then dropped. BbTextarea, BbDatePicker and BbInputGroupButton now render them. BbFormFieldCheckbox applies them in every orientation, BbFormSection whether or not it collapses, and BbFormFieldTimePicker on the picker rather than the surrounding field.
  • Dangling aria-describedby. BbFieldDescription and BbFieldError took an Id and never rendered it, so every BbFormField* control pointed its aria-describedby at an element that did not exist.
  • ARIA state attributes bound to booleans. aria-expanded, aria-hidden, aria-selected, aria-checked, aria-disabled and aria-current rendered with an empty value when true and vanished when false across Collapsible, Tabs, Menubar, DataGrid, Sidebar, Rating, Calendar and RangeSlider. They now render "true" and "false".
  • Keyboard traps and double activations. RadioGroup suppressed the default action of every key, so Tab could not leave the group. The Switch toggled twice per Space press, and a DropdownMenu trigger using AsChild opened and closed again on one Enter.
  • ToggleGroup semantics and focus. Single-select groups are a radiogroup of radio items reporting aria-checked, not buttons reporting aria-pressed. Arrow keys move from the item that actually holds focus, and items remove themselves from the group when they are removed from the page.
  • HoverCard. Moving the pointer from the trigger onto the card no longer closes it, and data-side is written in lower case so the slide-in animation matches.
  • NavigationMenu triggers each register their own slot; every one of them previously overwrote the last.
  • Parameters that did nothing now work: BbSelect.Open/OpenChanged, BbCommand.Disabled, BbCommandVirtualizedGroup.LazyLoadBatchSize, BbSlider.Orientation and BbRangeSlider.Orientation, BbResizablePanel.Collapsible, BbToastProvider.MaxToasts, BbDataGridHierarchyColumn.IndentSize, BbRadialBarChart.EndAngle, Color on BbPie and BbFunnel, BbSidebarMenuAction.ShowOnHover, the --sidebar-width-mobile token, and BbFill inside BbLine and BbRadar. Text and numeric inputs now push configuration changes to the browser after the first render.
  • BbNativeSelect with an enum. Choosing an option reported the enum's default, because the conversion went through Convert.ChangeType. A Value set before the user picked anything is also shown as selected.
  • Charts sharing one colour. ChartConfig.GetColor returned --chart-1 for any key the config did not name, so a partially configured chart drew every remaining series the same colour. It returns null now and the chart palette cycles.
  • DataGrid grouping with Virtualize and an ItemsProvider renders rows when a GroupedItemsProvider is supplied, instead of nothing at all.
  • Inline styles are merged, not replaced. A consumer style attribute on BbScrollArea, BbAspectRatio, BbSkeleton and the Command group/separator overwrote the component's own positioning and sizing.
  • BbQuantityStepper and BbNumericInput no longer wrap past the end of the numeric range: ArrowUp at int.MaxValue used to land on Min.
  • Native <dialog> without showModal() falls back to the JavaScript strategy rather than rendering a dialog with no modal behaviour.
  • Overlay options survive either registration order. AddBlazorBlueprintPrimitives(configureOverlays) before AddBlazorBlueprintComponents() silently lost the configuration; AddBlazorBlueprintComponents also takes configureOverlays directly now.
  • Hard-coded English in the Sortable announcements, the DataGrid column-visibility menu and filter buttons, and the FilterBuilder Apply/Clear buttons and group labels now goes through IBbLocalizer.
  • Primitives on their own. primitives.css defines the utility classes the primitives render, so a Primitives-only app no longer shows Sortable's screen-reader text as body copy or gets a Menubar overlay with no size.
  • Escape cancels a DashboardGrid pointer drag, which it had only ever done for a keyboard drag.
  • Resizable panels can be resized from the keyboard. Each handle is a role="separator" with aria-orientation, aria-controls and aria-value* reporting the panel in front of it, a localized aria-label (Resizable.ResizeHandle, overridable per handle with AriaLabel) and a tab stop. Arrows move 5%, Page Up/Down 20%, Home/End go to the minimum and maximum. It was a plain div with a pointer handler, while the documentation described all of the above.
  • InputConverter<T> parses enums. It went through Convert.ChangeType, which cannot produce one, so an enum-typed input fell through to "no converter registered".
  • The Dialog and Sheet primitive roots unsubscribe from their context. Both had a Dispose() the component never declared IDisposable for, so Blazor never called it.
  • NavigationMenu triggers unregister. The trigger list is keyed by the trigger rather than by a position, so a trigger removed from the page leaves with its entry and arrow-key navigation stops stepping onto a button that is no longer there.
  • More inline styles merged rather than replaced. BbResizablePanelGroup, BbResizablePanel, BbChartBase, BbDashboardGrid, BbDropdownMenuContent, and the Menubar content and DashboardWidget primitives let a consumer style overwrite their flex sizing, grid template or display: none.
  • Documentation corrections. BbTooltipTrigger.AsChild is documented as defaulting to false; the Tooltip, Dialog, Message, DataView, MultiSelect, CopyText and Carousel snippets compile and do what they claim; every localization example uses localizer.Set(...) rather than an options object that does not exist; candlestick click values are documented as [index, open, close, low, high]; the Resizable demo describes the accessibility it has rather than the accessibility it does not; THEMING.md explains that :root { --font-sans } overrides ThemeFont; and the WebAssembly demo host loads themes.css like the other two.
  • Modal keyboard containment. Tab and Shift+Tab enter the trapped controls when initial focus is on a container or non-tabbable listbox, preventing WebKit from moving focus behind a Select bottom sheet. Empty modals retain focus on their container.
  • Drawer focus restoration. Closing a drawer with Escape, its close button, the backdrop or a dismiss gesture returns focus to the trigger that opened it, including composed triggers. Navigating away does not restore focus into the departing page.
  • Menu demo documentation. Remove empty secondary API headings from Dropdown Menu, Context Menu and Menubar; their references remain in the final API section.
  • Scheduler vertical scrolling. Demo schedules now include all 24 hours and initially scroll to 8 AM, instead of omitting hours outside short example ranges. The scroll region fits the viewport, retains sticky day headings and can receive keyboard focus. InitialScrollHour sets the initial display-zone hour without changing the rendered range or resetting scrolling on later renders.
  • Cached core scripts. Revise both the core JavaScript entry URL and its relative dependency URLs so stale sidebar exports cannot disable an upgraded demo circuit.
  • Demo sidebar sizing. Restore the standard sidebar width and align v4 badges at the trailing edge of each component row. Every badge sits on one vertical line, in the sidebar's own right gutter: a sub-menu insets both edges and a nested one insets again, so the right inset is dropped at each level. Long labels stay on one line and end in an ellipsis, with the full name on the row's title.
  • Mobile Shop recipe and focused component demos. Move the combined shopping experience into Recipes with separate product browsing, cart editing and account screens. App Bar, Bottom Navigation, Notification Badge, Quantity Stepper and Section Header each have their own demo. Motion, Height Animation, Selection Indicator, Page Transition, Screen Transition and Render State Provider also have separate pages. The previous mobile-controls URL remains a recipe alias.
  • Scheduler repeat editor. Daily, monthly and yearly choices no longer ask for an interval. Weekly events offer labelled weekday checkboxes, restore saved selections and require at least one day. Monthly/yearly repeats use the start date. Replace the raw custom-rule input with plain-language preservation of existing advanced schedules, which are kept until explicitly replaced. Optional occurrence limits remain available.
  • Demo documentation review. Restore the original main-menu order while keeping Components and related pages alphabetized. Standardize accessibility and keyboard guidance across component demos, fill missing API references, consolidate stray API cards beneath one heading, and correct stale Tooltip placement and FieldError collection types. New APIs carry v4 badges; established components such as Sortable do not gain a new-component sidebar badge.
  • TimeInput selectors. Use BbSelect for AM/PM both inline and in the clock popup, and for the popup’s numeric choices. Preselected values display correctly, and period changes preserve minutes and obey read-only/disabled state.
  • Scheduler navigation and editor. Center the date heading and group Previous, Today and Next together. Today navigates to the current date in the schedule's time zone while preserving the view and week-start choice. Clicking the editor backdrop keeps the draft open.
  • Sidebar demo organization. Place floating pill navigation and animated selection examples alongside the collapse/navigation examples, each with its code snippet. Keep accessibility guidance after the examples and the new component APIs in the final API reference.
  • TreeSelect keyboard actions. Space expands/collapses the focused branch without selecting it. Enter selects and closes in single mode, including the current selection; in checkbox mode, it checks/unchecks the item while keeping the popup open. LeafOnly selection rules and standalone TreeView keyboard behavior are preserved. Revised the primitive bundle and tree-keyboard asset URLs so cached scripts do not retain the previous key mapping.
  • Generate Tailwind CSS before static asset discovery, fingerprinting and compression, including CSS missing on a clean checkout. This prevents stale asset metadata and empty compressed stylesheet responses in the demo hosts. Added a check for all hosts and optional live HTTP delivery.
  • DataGrid editors preserve column widths. Inline BbInputGroup save/cancel actions fit inside the existing cell without widening the column or moving neighbouring columns.
  • Hierarchy pickers match the standard Bb controls. TreeSelect and Cascader use consistent chevrons, search rows and focus styling. Parent selection reaches collapsed/filtered descendants, and closing a TreeSelect preserves its search through the exit animation to avoid flicker.
  • Scheduler event layout and scrolling. Appointment content is top aligned with spacing between overlapping events. Native component scrollbars follow the light/dark theme.
  • Picker focus and overlay dismissal. Date pickers restore focus after selection, Enter opens a Select without a second native click closing it, and overlay exit waits ignore unrelated child animations.
  • A vertical slider now lays out vertically. Both BbSlider and BbRangeSlider put the track in a row container, and the track carries grow, which follows the flex main axis — so a slider asked for vertically stretched its 8px track across the full width of its container instead. Vertical sliders stack on the cross axis, and the fill is pinned to the bottom edge it grows from. BbSlider.Orientation was read nowhere at all: the value reached the primitive, which already handled vertical, but the styled layer drew the result horizontally either way. BbRangeSlider shows its value tooltips to the right of a vertical track and its tick marks to the left, so the two cannot overlap. Give the container a height; the slider fills it.
  • Consumer group-* and peer-* variants reach the library again. Tailwind resolves those variants against a literal marker class, and a consumer's own build emits :where(.group) — never :where(.bb\:group), because it never sees the bb prefix. Every marker the library shipped carried the prefix alone, across 30 components, so anything a consumer hooked onto one silently matched nothing: a collapsed BbSidebar kept the labels and chevrons group-data-[collapsible=icon]:hidden was meant to hide, a label next to a disabled BbCheckbox or BbSwitch never dimmed, and a BbCollapsibleTrigger chevron never rotated — all visible in the demo's own pages. ClassNames.cn now carries the bare twin of any marker it keeps, named ones (peer/menu-button) included; a group-* variant is an ordinary utility and still ships prefixed only, which is what #496 was about. A marker handed straight to a component's Class parameter is the one path the merge cannot see, because a primitive concatenates Class verbatim — BbDataGrid's header row did exactly that, and a convention test now fails on it.
Documentation and Packaging
  • The BbPortalHost setup snippets carry their @using, and the missing-host warning checks it first. An unresolved Razor tag is not a build error, so a missing using turned <BbPortalHost /> into a literal <bbportalhost> element and every overlay silently stopped rendering. Alongside the namespace move above, both READMEs now show the using in the layout snippet and name the symptom, and the warning walks through the @using check — including the v3 namespace — before anything about render modes (#545).
  • Getting Started dialog snippets name components that exist. The page showed <Dialog>, <DialogTrigger> and the rest under @using BlazorBlueprint.Components.BbDialog, which is not a namespace — every tag was Bb-prefixed and the using is BlazorBlueprint.Components. The primitive snippet mixed a fully qualified root with unqualified children; it now shows one @using and notes that primitives and components share type names, so a file imports one namespace or fully qualifies the tags. Two Next Steps links pointed at routes that no longer exist.
  • Previous/next component navigation in the demo header. Two buttons beside the sidebar trigger step through ComponentCatalog.DemoPages, the same flattened list the sidebar, the component homepage and the command search already walk, so "next" always means the next row the reader can see. They disable at the ends and render nothing off that list — on the homepage, a guide or a primitive page there is no meaningful neighbour to offer.
  • Slider gains a vertical example with a live demo, a copyable snippet and an Orientation API reference entry. The Controlled Sidebar demo wraps its menu labels in a span, which is what icon mode hides.
  • More snippets naming components that do not exist. The Lucide, Heroicons, Feather and Font Awesome pages showed <Button> where the live example beside them used <BbButton>, and a CopyText note asked for <PortalHost /> rather than <BbPortalHost />.
  • Dead demo links and a broken image. The Primitives index advertised a Combobox primitive; there is no such primitive, only the styled BbCombobox. The Sidebar demo's avatar Source was relative to the page rather than the base path, so it 404'd and always fell back to the initial. The ResponsiveNav demo's /docs and /login links now use the # placeholder the rest of that file already uses.
  • Add permanent DataGrid editing, Scheduler, TreeSelect, Cascader and upload lifecycle demos with code examples, accessibility notes and API references. New pages appear in the sidebar, command search and component catalog; editing demos cover several input types, validation recovery and ordering after saves. See PR #536.
  • Complete README acknowledgments and include library licenses and upstream notices in all six NuGet packages. Bundle browser-library notices as static assets, restore Floating UI and tw-animate-css license headers, and retain ECharts subcomponent notices.
  • Correct Lucide and Font Awesome package license expressions to account for their embedded artwork, while retaining MIT for their C# wrappers.
Performance
  • Paged DataGrid queries fetch the requested page. For IQueryable sources without client-side search, grouping, hierarchy or row virtualization, counting and Skip/Take stay on the query provider. Full-data CSV exports run the filtered, sorted query on demand; keep its provider alive until export. ItemsProvider remains the asynchronous option for remote data, and formatted search and grouping retain their full-data behavior.
  • Command search shares filtered results and an item-index lookup. Visible items no longer each rebuild and scan the whole filtered list. Search, filter and item changes invalidate the cache; callback-only changes do not.
  • Slider, RangeSlider and ColorPicker drag feedback is immediate in the browser. Live .NET updates are coalesced to the latest distinct value, normally no more than every 50 ms, with one callback in flight. Release and cancellation flush the final value. RangeSlider captures pointers locally; Rating hover updates on entering an icon instead of every mouse move. Public component parameters are unchanged.
  • TreeView search indexes parents and visible nodes. Matching siblings share ancestor lookups, rendering no longer repeats descendant searches, and cached lazy children remain searchable after parent renders.

September 16, 2026

Added
  • Tables in BbRichTextEditor — The Full toolbar gains a table button. Outside a table it opens a size picker (up to 6 × 6); with the caret inside a table the same button offers insert row above/below, insert column left/right, delete row, delete column and delete table. The same actions are on the component reference: InsertTableAsync(rows, columns), InsertRowAboveAsync, InsertRowBelowAsync, InsertColumnLeftAsync, InsertColumnRightAsync, DeleteRowAsync, DeleteColumnAsync and DeleteTableAsync. This is Quill 2's own built-in table module, so there is no new script to load; the HTML output is a plain <table>, cell borders follow --border, and the module's Tab, Enter and Backspace behaviour inside cells applies. Cell merging and column resizing are not part of Quill's module and are not offered. New RichTextEditor.Table*, InsertRow*, InsertColumn* and Delete* localizer keys carry the labels.
  • Undo/redo, checklist, inline code, alignment, text colour, highlight and images in BbRichTextEditor. Standard gains undo/redo buttons (Quill's history module, userOnly so a programmatic Value update is never undoable; the buttons grey out when there is nothing to do, and TextChangeEventArgs reports CanUndo/CanRedo) and a checklist toggle. Full also gains inline code, an alignment menu, text colour and highlight palettes, and an image button. Alignment uses Quill's style attributor so the output carries text-align inline rather than ql-align-* classes that mean nothing outside Quill's stylesheet; colour and highlight are inline styles too. Checklist state round-trips: Quill writes <li data-list="checked"> but only reads data-checked on the list back in, so the interop adds a clipboard matcher for the item and the sanitizer keeps data-list. Images go through the new ImageUploader parameter — every picked, dropped or pasted file is streamed to it and it returns the URL to embed, or null to reject; MaxImageSize (10 MB) caps what is sent. Without a handler, images embed as data URLs, as Quill does on its own, and the sanitizer now lets data:image/* through on <img src> only. UndoAsync, RedoAsync and InsertImageAsync(url) join the public methods; EditorImageUpload is the new public type.
Changed
  • table, code, align, color, background and image are now registered formats in BbRichTextEditor. Pasted or bound HTML that carries them keeps them, where it used to flatten to plain paragraphs and text. HtmlSanitizer's defaults already allow the table elements and the inline styles involved.
  • Quill is pinned to 2.0.3 in the demo and setup notes ([email protected] instead of the floating quill@2 tag), and the interop no longer carries Quill 1 fallbacks for getSemanticHTML. The editor has required Quill 2 since it was rewritten for it; the fallbacks only hid a wrong script version behind subtly different HTML.
  • BREAKING — every Tailwind utility in blazorblueprint.css is now prefixed bb:#501, fixing #496. The prebuilt stylesheet wrote its utilities into Tailwind's utilities cascade layer, the same layer a consumer's own build writes into. Layer names are global to the document, so two builds emitting the same class name were ordered by which <link> came second, not by Tailwind's sort order. A consumer's sm:grid-cols-2 md:grid-cols-4 collapsed when the library loaded after their stylesheet; the library's own hidden sm:flex collapsed when it loaded before. No load order fixed both, and the demo had been shipping in the broken arrangement.
Fixed
  • Navigating away from an open overlay no longer logs overlay: close callback failed. Clicking a link while a select, popover or menu was open closed the overlay and unloaded the page in the same gesture. The close asked the browser for a JsOnClosed callback once the exit animation finished; the navigation disposed the component before the animation ended, and because the portal already counted itself closed, disposal skipped its own close call and released the .NET reference the callback was about to use. Nothing broke — the callback only unregisters content that disposal had already unregistered — but every such navigation put a System.ArgumentException: There is no tracked object in the console. The portal now remembers that a callback is outstanding and sends one more close on disposal, which cancels the pending one on the browser side before the reference goes away. Introduced with the one-round-trip close in 4.0.0-beta.1.

September 15, 2026

Added
  • JsModules.TryGetLoaded and PrimitiveModules.TryGetLoaded — get an already-imported module without awaiting. For callers that must issue interop from a synchronous pass, where awaiting the import would put the call a round trip behind the render it needs to travel with. Returning false is "not yet", not an error: the caller falls back to the awaited path, which pays one round trip and warms the cache for every call after it.
  • BbFloatingPortal.SideElementId — names the element that carries the resolved data-side, written by JS as soon as the overlay is positioned and kept current when a scroll flips it.
  • BbFloatingPortal.Keyboard (FloatingKeyboardOptions, FloatingKeyboardKind) — declares listbox or menu key handling to be wired inside the open call and released inside the close.
  • BbFloatingPortal.ScrollToCurrentIn and ScrollToCurrentSelector, surfaced on both layers of BbPopoverContent as ScrollToSelected and ScrollToSelectedSelector — scroll the chosen item into view each time the overlay opens, before it is revealed.
Changed
  • Moving the pointer over a command or combobox list no longer talks to the server per pixel. Every BbCommandItem bound @onmouseenter and @onmousemove, so on Blazor Server every pixel of pointer travel was a circuit message and every item crossed re-rendered the whole list. The mousemove existed only to tell a real hover from the list scrolling under a stationary pointer during keyboard navigation. The browser now answers that itself: one delegated listener per list (elementUtils.observeHover) and one call to .NET per item the pointer genuinely moves onto. Measured across five combobox items at one pixel a step: 157 pointer steps → 11 messages, where it was one per step. BbCommandItem also gains a ShouldRender, so a focus move re-renders the two items it touches rather than all of them.
  • Scrolling an infinite-scroll list no longer costs a round trip per scroll event. BbCommandList, BbSelectContent, BbMultiSelect and BbDataView bound @onscroll and then awaited isNearBottom in JS — around sixty circuit messages a second while the wheel turned, each waiting a further round trip for the answer. elementUtils.observeNearBottom watches the element in the browser and calls .NET exactly once when the scroll position enters the near-bottom zone, re-arming when it leaves or when the content grows.
  • Focus happens inside the open and close calls, not a round trip after. BbCombobox and BbMultiSelect focused their search box from the popover's ready callback — a round trip after the content rendered, then a 50ms sleep, then another round trip for FocusAsync. The new BbPopoverContent.AutoFocusId (and BbFloatingPortal.AutoFocusId) focuses the element one frame after the reveal, inside the call that positions the overlay. BbCommandInput gains an Id so an owner can name it. On the way out, Select, Popover and DropdownMenu awaited FocusManager.RestoreFocus after their close render on every Escape and every selection; RestoreFocusToId and RestoreFocusOnClose ride along with overlay.close and the browser puts focus back on the trigger itself. BbNavigationMenuTrigger's ArrowDown no longer sleeps 50ms before focusing the first item.
  • BbPopoverContent only asks for the portal's ready callback when a consumer set OnContentReady. Nothing in the library listens to Context.OnContentReady, so the callback — ack-gated, and a re-render of its receiver — was a round trip spent notifying no one.
  • An overlay opens in one circuit round trip, and closes in one. Measured on a real deployment at a ~90ms round trip, opening a BbSelect on 3.14.1 cost 11 sequential round trips — 1.6 seconds, and 5.5 seconds on a 500ms circuit. 4.0.0-beta.1 cut that to 5. It is now 1, and the arrow keys and hover cost none at all.
  • JavaScript owns the overlay's position, its side, and a listbox's highlight. These were all C#-rendered, and each one was a render batch — a round trip — to record a measurement the browser had already made.
  • Keyboard wiring happens inside the call that opens the overlay. The new FloatingKeyboardOptions covers both a listbox and a menu; BbFloatingPortal attaches the handlers on the way open and releases them on the way closed, in the same calls that position and hide the element. BbSelectContent and BbDropdownMenuContent declare what they need rather than wiring it from a ready callback, which used to leave the overlay on screen and deaf to the arrow keys for a further round trip.
  • The command search box shows focus as a band, not a box inside a box. BbCommandInput drew a focus ring around the <input>, which sat inside a bordered row, which sat inside a bordered popover — three nested outlines with gaps between them. It was not an occasional look either: a combobox focuses its search field every time it opens, so the ring was on screen the whole time the dropdown was, marking the only element that could possibly have focus.
  • BbDropdownMenuContent no longer calls matchTriggerWidth. MatchAnchorWidth already asks Floating UI's size middleware for the same width on every position pass, and autoUpdate re-runs that on resize. The separate call was a second writer of one style, bought with an awaited interop call that also allocated a JS object reference to dispose later.
  • BbTooltipContent and BbHoverCardContent no longer ask for the portal's ready callback. Both handlers were empty, and on Server the callback is ack-gated, so asking for it cost a round trip to do nothing.
Fixed
  • No async void handler can take the circuit down. Eight fire-and-forget handlers — close timers, debounce, state-change and location-change subscribers — caught one or two exceptions and let the rest escape. Nothing awaits an async void method, so an escaping exception has no caller to reach and Blazor Server treats it as fatal. The sidebar's scroll-to-top was one of these. Every one now catches everything; each does best-effort work and none of it is worth a dead circuit.
  • A stale cached JavaScript module fails loudly, and a new release is a new URL. A consumer behind a CDN with a long browser-cache TTL deployed a new build and got a four-hour-old sidebar.js under the new bundle; sidebar.initialize was not a function and every circuit died with nothing to say why. The C# import of each bundle now carries the library version as a query (PrimitiveModules.ModuleUrl, the new ComponentModules.CoreUrl, built by JsModules.Versioned), so a cached entry file is never a valid answer for a new release. And each bundle asserts one export it depends on from every module it imports, at load, throwing an error that names the stale file and says what to do — the import rejects, consumers catch it and degrade, and the console says which file is old.
  • Navigating between pages no longer kills the circuit. BbSidebarInset scrolls the main area to the top on every navigation, and it called scrollToTop — the name the function had before the five common modules were bundled into bb-components-core.js. The bundle re-exports each module under its file name in camelCase, so the bare identifier resolved to nothing and every client-side navigation threw Could not find 'scrollToTop'. The handler is async void, so the exception had nowhere to go and took the connection down with it; a reload fixed it until the next link click.
  • A closing overlay no longer blinks back into view before it disappears. The dropdown faded out, snapped back to full opacity for a fifth of a second, and only then vanished.
  • BbCombobox reopens scrolled to the chosen item. Pick a country a long way down the list, close the combobox, open it again, and it came back at the top of the list with the selection out of sight. BbSelect has always scrolled to its selection; the combobox never had the behaviour at all.
  • BbCombobox no longer discards a paged list when it closes. Closing it told the consumer the search had been cleared even when nothing had been typed, which a paged list correctly reads as "reload your first page". Scroll in seven pages, pick an item from the last of them, and closing threw all seven away: reopening showed page one, and the chosen item was no longer in the list at all. The notification now fires only when there is a search to clear.
  • A closing overlay's exit animation is no longer cut short by its own unmount. Content that is unmounted on close (ForceMount="false") has to survive long enough to animate, so the unmount can no longer share the close's render. JS reports back once the animation has finished and the element is hidden, and the unmount happens then — one round trip later, behind an overlay the user already cannot see. Content that stays mounted needs no callback at all.
Known
  • Popover and dropdown menu still open in two round trips, not one. The second is a portal-host flush: the content's registration and its root's unconditional re-render both reach the host in the same render batch, and the host — unable to tell a same-batch refresh from a genuine mid-cycle update (#418) — defers a flush that lands one round trip after the overlay is already on screen. Removing the content's own open-side StateHasChanged did not clear it. The fix needs the host to know where a render batch ends, which Blazor does not expose; parked with that analysis.

September 14, 2026

Added
  • BbCopyText can take an asynchronous value#466, the deferred half of #453. ValueFuncAsync is for text that has to be fetched or computed. Value still wins when non-empty, then ValueFunc, then this.
  • BbCopyText reports a failed copyOnCopyFailed carries a CopyTextFailure of Refused (the browser rejected the write) or NoValue (nothing to copy). Failure used to be entirely invisible, which is exactly what let the bug above go unnoticed for so long.
Changed
  • Theme and sidebar start up in one call each. ThemeService.InitializeAsync made two to four separate calls — read localStorage, maybe clear a stale entry, maybe ask the OS for its dark-mode preference, apply the result — and BbSidebarProvider made two. Every one was a circuit round trip, on every page load, before the page could settle into the right state. None of the decisions between them need the server: they are reads of a cookie, localStorage and a media query.
  • The five modules that load on nearly every page ship as one file. bb-components-core.js re-exports theme, sidebar, sidebar-inset, text-input and composition-guard, addressed as textInput.initialize in the same style as the primitives bundle.
  • A data grid attaches its row key and click handlers once, not once per row. Every BbDataGridRow registered its own listeners in its OnAfterRenderAsync, and disposed them one by one on teardown — one interop call, and so one circuit round trip, per row in each direction. The cost scaled with row count, which is why a large grid felt slow and a demo page never did.
  • A component's JavaScript module is imported once per circuit, not once per component. Every component cached its module reference in an instance field, so thirteen BbInputs on a page issued thirteen import calls for the same already-loaded file — and on Blazor Server each one is a network round trip. The cost scaled with how many controls a page had, which is why it showed up on dense admin forms and not in a demo.
  • A select prepares its listbox in one call. Scrolling the selected option into view and attaching the keyboard handler were two separately awaited calls, so two more circuit round trips on every open, while the user waited for the list to become usable. select.openListbox does both.
  • Opening an overlay costs one interop call, not five. Every InvokeAsync from C# on Blazor Server is a message the server posts to the browser and then awaits, so it costs a network round trip — paid on every open, forever, not just the first.
  • Breaking — JsOnClickOutside and JsOnEscapeKey are gone from BbPopoverContent, BbSelectContent and BbDropdownMenuContent. Both were [JSInvokable] and [EditorBrowsable(Never)] — callable only from the library's own JavaScript. Dismissal now arrives through BbFloatingPortal.OnDismiss.
  • Escape dismisses the topmost overlay, not every open one. A popover opened inside a dialog used to close the dialog on the first press — sometimes both at once — because each overlay registered its own document-level keydown listener and none of them knew about the others.
  • Removed — onClickOutside and onEscapeKey in click-outside.js. Both are superseded and both caused a bug fixed in this release: the first went stale when Blazor replaced the element and had no exemption for nested portals, the second was the second document listener that broke Escape ordering. Neither has a caller left. Use onClickOutsideByIds and escapeKeydown.initialize.
  • BbFloatingPortal no longer gives up on the portal host after 500ms. MountPortalAsync raced the host's render signal against a Task.Delay(500). The signal is never lost — it arrives exactly one network round trip after the portal registers, because the host only reaches OnAfterRenderAsync once the browser has acknowledged the render batch. Measured, the wait tracks round-trip time 1:1: 104ms at a 100ms round trip, 305ms at 300ms, 488ms at 480ms, and at 600ms every single open times out. That is what produced the stray PortalRenderTimeout warnings — not a lost signal, a fixed budget for a variable cost.
  • The execCommand clipboard fallback no longer runs for every failure — it ran whenever navigator.clipboard.writeText threw, which meant an expired user activation quietly fell through to a path that cannot rescue one either, and success was reported regardless. It now runs only for the insecure-context case it was written for, where the Clipboard API is absent altogether.

September 13, 2026

Changed
  • BREAKING — BbTooltipTrigger.AsChild now defaults to false#428, the deferred half of #425. It defaulted to true, where the trigger renders no element and no handlers and only cascades a TriggerContext for the child to consume. BbButton consumes it; LucideIcon and plain markup do not. So the most natural thing a consumer writes — a bare icon in a trigger — silently did nothing, and the usage that worked required knowing about an opt-out. That is the wrong way round for a component library, and it was reported from the field.
Added
  • Charts report clicks#480, requested by @netclectic for drilling down into the underlying data. Every chart type gains OnDataPointClick and OnChartClick through BbChartBase, so bar, line, area, pie, scatter, candlestick and the rest all get them at once.
  • BbFileUpload accepts pasted files#485, requested by @HugoVG, whose point was that not everyone wants to drag onto a dropzone. Copy a file in your file manager, or take a screenshot to the clipboard, focus the upload and press Ctrl+V.
  • A way to apply the saved theme before the first paint#477, reported by @andrewbabbittdev. The theme lives in localStorage, so a prerendered or statically rendered page cannot know it: the server renders the default and the saved theme is applied once Blazor has started. A user who chose dark mode got a flash of light first, on every load. There was no built-in answer, and there is no C#-only one — the preference is not available to the server at render time.
Fixed
  • The last components showing the browser's own focus outline — final tranche of #459 on develop. BbCarouselNext, BbCarouselPrevious, BbRating, BbMarker, BbSidebarRail and BbFileUpload.
  • BbInputGroupAddon no longer has a click handler that does nothing — found while finishing #459. The addon rendered @onclick="HandleClick", and the handler was a stub: a comment reading "In a real implementation, we'd use ElementReference and JSInterop" followed by await Task.CompletedTask. Its documentation claimed it focused the sibling input like a native label. It never did.
  • Four more components showed the browser's own focus outline — third tranche of #459. BbDateTimePicker, BbFormWizard, BbThemeSwitcher and BbColorPicker.

September 12, 2026

Changed
  • BREAKING — BbDrawerTrigger and BbDrawerClose now render a real <button>#507, found while working #459. Both were a bare <div @onclick> with no tabindex, no role and no keyboard handler. They worked when the child happened to be focusable — which is what the demos did, wrapping a BbButton, so the common path was fine and this went unreported. Pass anything that is not itself focusable, which the API placed no constraint on, and the trigger was unreachable by keyboard and not exposed as a control at all: a WCAG 2.1.1 failure in a shape the component invited, failing silently and passing any mouse test.
Fixed
  • BbCopyText's tooltip reappeared and stuck when you came back to the browser tab#506, reported by @garrenf with a video. The last BbCopyText you clicked would show its tooltip again on returning to the tab, and nothing dismissed it: hovering the text and leaving again was the only way out.
  • Five more components showed the browser's own focus outline, and the focus-indicator allowlist is now empty — second tranche of #459, after #508. BbBreadcrumbLink, BbAttachmentTrigger, BbCommandInput, BbCopyText and BbResponsiveNavItems.