Version v3 Documentation
Migration Guide
Everything you need to upgrade from Blazor Blueprint v3 to v4.
Migration Checklist
Breaking changes that require code updates. Address them in order.
| # | Breaking Change | Severity | Action Required |
|---|---|---|---|
| 0 | .NET 10 minimum for all Bb packages | High | Retarget applications to net10.0 or later; .NET 8/9 cannot consume v4 |
| 1 | BbDrawerTrigger / BbDrawerClose render a real <button> |
Medium | Add AsChild="true" where the child is already a control |
| 2 | BbTooltipTrigger.AsChild default → false |
Medium | Add AsChild="true" where the child consumes the trigger context, such as a BbButton |
| 3 | Every utility in blazorblueprint.css is prefixed bb: |
Low for most; Medium if you relied on the shipped utilities without your own Tailwind build | Running Tailwind: set the default border colour in your own stylesheet (one @layer base rule). Not running it: see below |
| 4 | The portal host components moved to the BlazorBlueprint.Primitives namespace |
Low | Nothing if your _Imports.razor already has @using BlazorBlueprint.Primitives. Otherwise add it |
| 5 | NavigationMenuContext trigger registration is keyed by the trigger |
Low | Only affects code that drives the primitive directly. Pass the component instead of an index |
| 6 | Parameters that never did anything are gone | Low | Delete them. None of them changed any behaviour |
.NET 10 minimum
Blazor Blueprint v4 requires .NET 10 or later. .NET 8 and .NET 9 are no longer supported. Install .NET 10 (the source repository uses SDK 10.0.400 or a later feature band), change consuming projects to net10.0, and update their Microsoft.AspNetCore.Components package references to 10.0.x before upgrading Bb. The library, icon packages, demo hosts and tests all target net10.0.
This guide helps you upgrade from Blazor Blueprint v3 to v4. Breaking changes that require code updates come first, followed by anything you can adopt at your own pace.
This guide is written as v4 is built, so it grows as changes land.
2. BbTooltipTrigger.AsChild now defaults to false
Issue: #428, deferred half of #425
In v3 this defaulted to true. In that mode the trigger renders no element and no handlers — it
only cascades a TriggerContext, and the child is required to consume it. BbButton does;
LucideIcon and plain markup do not. So the most natural thing to write silently did nothing:
<BbTooltipTrigger>
<LucideIcon Name="house" />
</BbTooltipTrigger>
A default where the obvious usage fails, and the working usage requires knowing about an opt-out,
is the wrong way round. v3 moved trigger defaults to true across the family; for tooltip
specifically that turned out to be the wrong call, and v4 reverses it.
What to change
Add AsChild="true" wherever the child consumes the trigger context itself:
<!-- v3 -->
<BbTooltipTrigger>
<BbButton Variant="ButtonVariant.Outline">Hover me</BbButton>
</BbTooltipTrigger>
<!-- v4 -->
<BbTooltipTrigger AsChild="true">
<BbButton Variant="ButtonVariant.Outline">Hover me</BbButton>
</BbTooltipTrigger>
A bare icon, plain text or arbitrary markup needs no change, and now works.
What actually changes in the DOM
With AsChild="false" the trigger wraps its content in two nested <span> elements, not one:
<span class="bb:contents"> <!-- styled wrapper: display: contents, no layout box -->
<span id="…" tabindex="0" <!-- the primitive trigger: an ordinary inline span -->
aria-describedby="…"> <!-- this is what carries the hover/focus handlers -->
…your content…
</span>
</span>
The outer span uses display: contents, so it generates no layout box. The inner one does not — it
is an ordinary inline element, so it does establish a box. In flow layout that is usually
invisible, but inside a flex or grid container it becomes the flex item instead of your content,
and inline-block sizing applies to it rather than to the child. Set Class on the trigger to
give that wrapper the layout you need.
Anything that walks the DOM is affected either way: :first-child selectors, querySelector paths
and test hooks that assume the child is a direct descendant.
Only the styled wrapper changed
BlazorBlueprint.Primitives.Tooltip.BbTooltipTrigger already defaulted to false. The divergence
was in the Components layer, and this removes it.
The warning is still there
An unconsumed trigger context is reported through ILogger in the Development environment, added
in #425. With the default flipped you should
see it far less often, but it still catches an AsChild="true" around a child that ignores the
context.
Other AsChild triggers
#428 asked whether the same default question applies to popover, dialog, sheet, dropdown menu, hover card and collapsible. It does not, and they are unchanged:
Those triggers render a <button> in their non-AsChild branch and are opened by a click, which
any focusable child already delivers by bubbling. Tooltip is different because it opens on hover
and focus, which do not bubble usefully — so a trigger that renders nothing genuinely has nothing
listening. The asymmetry is in the interaction, not in the API.
3. Every utility in blazorblueprint.css is prefixed bb:
blazorblueprint.css is a prebuilt Tailwind stylesheet. In v3 its utilities were unprefixed and
written into Tailwind's utilities cascade layer — the same layer your own Tailwind build writes
into. Layer names are global to the document, so two builds emitting the same class name into the
same layer were resolved by which <link> came second, not by Tailwind's sort order. Your
sm:grid-cols-2 md:grid-cols-4 collapsed when Blazor Blueprint loaded after your stylesheet, and
the library's own hidden sm:flex collapsed when it loaded before. No load order fixed both.
In v4 every utility the library emits is prefixed — .bb\:flex, .bb\:sm\:hidden,
.bb\:data-\[state\=open\]\:bg-accent — and lives in a bb-utilities layer of its own. The two
builds can no longer produce the same class name, so nothing depends on load order any more.
If you run your own Tailwind build
Nothing changes in your markup. Class="p-6" is still p-6; the library strips its prefix
when it merges, so your unprefixed class still replaces the library's for the same property:
<BbCard Class="p-6"> @* renders class="… bb:rounded-lg bb:border … p-6" *@
Three things to check:
Remove any
@sourcethat points at the Blazor Blueprint package or sources. It was never needed, and under v4 it findsbb:flex, does not recognise thebbvariant, and emits nothing.Load order no longer matters for utilities. Keep your theme before
blazorblueprint.cssas before; put your Tailwind output wherever you like.Set the default border colour in your own stylesheet. Add this to the file that holds your
@import "tailwindcss":@layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--border); } }Without it, your own
border,border-b,border-tand so on render in the text colour — near-black in light mode — instead of the theme's grey. Library components are unaffected: they usebb:-prefixed utilities that carry their own colour.And declare the dark variant in the same file:
@custom-variant dark (&:where(.dark, .dark *));The library switches dark mode by putting
.darkon<html>, but Tailwind v4 defaultsdark:to@media (prefers-color-scheme: dark). Without this your owndark:classes follow the operating system rather than the toggle, so they apply at the wrong times whenever the two disagree.Why your stylesheet and not ours. Tailwind's preflight sets
border: 0 solid, and because that is a shorthand it resetsborder-colortocurrentColor. Both stylesheets write their preflight into the sharedbaselayer at the same specificity, so the one that loads last wins — and that is yours. The library's own default is discarded for the whole document. The rule has to live in the stylesheet that loads last, which is why we cannot ship it for you.This is not new in v4. The library has put its default in
@layer basesince at least 3.17.0, so the collision existed there too. It surfaces now because v4 is the release that makes running your own Tailwind build alongside the library safe, so more people do it — and because upgrading is when you rebuild your CSS and notice.If you already followed the shadcn setup, you have this line and there is nothing to do.
If you do not run Tailwind
Some projects wrote Tailwind classes in their own markup and relied on blazorblueprint.css
happening to contain them. That was never supported — the file only ever held the classes the
components use — and in v4 those classes are all prefixed, so a bare class="flex gap-4" in your
page matches nothing.
You have two options:
- Add a Tailwind build to your project. This is the supported path for using utilities in your own markup. The standalone CLI needs no Node.js.
- Use the prefixed classes directly:
class="bb:flex bb:gap-4". They work anywhere on the page, not only inside components. The set is whatever the components happen to use and may change between versions, so treat this as a stopgap rather than an API.
Renamed: shimmer and scroll-fade-x
These two utilities were safelisted so consumers could apply them by name. They are now
bb:shimmer and bb:scroll-fade-x:
<!-- v3 -->
<BbMarkerContent Class="shimmer">…</BbMarkerContent>
<!-- v4 -->
<BbMarkerContent Class="bb:shimmer">…</BbMarkerContent>
Internal class names are not an API
If you have CSS, JavaScript or tests that select the library's internal elements by utility class
(.flex-col, .group\/row, .hidden), those selectors now need the prefix. Prefer the data-slot
and other data attributes the components render; those are stable.
4. Portal host components moved to BlazorBlueprint.Primitives
BbPortalHost, BbContainerPortalHost, BbOverlayPortalHost and BbCategoryPortalHost were in
BlazorBlueprint.Primitives.Services. They are now in BlazorBlueprint.Primitives, alongside every
other primitive component. The services themselves — IPortalService, PortalService,
PortalCategory — have not moved.
What to change
Nothing, if your _Imports.razor follows the documented setup:
@using BlazorBlueprint.Components
@using BlazorBlueprint.Primitives
If a file imports only BlazorBlueprint.Primitives.Services and writes <BbPortalHost />, add
@using BlazorBlueprint.Primitives to it.
Why this is worth a breaking change
A Razor tag that does not resolve to a component is not an error. The compiler emits it as a literal
HTML element, so <BbPortalHost /> became <bbportalhost>: no host registered, no overlay ever
rendered, and no build output pointing at the cause. The only visible symptom was a runtime warning
saying the host was missing from a layout that plainly contained one. Reported in
#545, where the giveaway was that adding
@rendermode to the tag failed with RZ10023: Attribute '@rendermode' is only valid when used on a component.
Putting the host in the namespace people already import removes the trap. Two other things guard it
now: the warning walks through the @using check first, and both READMEs carry the using in their
setup snippets.
This is the same move v3 made for eight other consumer-facing types, which is why a v3 application that followed that migration already has the right using.
6. Parameters that never did anything are gone
Each of these was accepted and then ignored. Removing them changes no behaviour — it just turns a silent no-op into a compile error. Delete the attribute.
| Removed | Why it did nothing | What to use instead |
|---|---|---|
BbCalendar.Mode and the CalendarMode enum |
BbCalendar is single-select; the mode was never read |
BbDateRangePicker for a range |
BbCommand.CloseOnSelect |
The dialog is what closes, not the command list | BbCommandDialog.CloseOnSelect |
Stacked and StackGroup on BbPie, BbFunnel, BbGauge, BbRadar, BbHeatmap and BbCandlestick |
None of these series types can stack | Nothing — stacking now lives on StackableSeriesBase, so the parameters appear only on BbBar, BbLine, BbArea, BbScatter and BbRadialBar, where they work |
These are the only public members removed in v4. Everything else in the surface is additive or a namespace move (see 4).
New v4 editing and scheduling APIs
BbDataGridaddsDataGridEditMode.CellandBatch. SupplyEditItemFactoryto make independent editable DTO copies, including nested objects. ExistingRowmode continues binding to the original row. Cell callbacks receive a draft asItemand the source asOriginalItem; batch callbacks receive allChangesand must persist them atomically. Failed validation or rejected saves retain drafts. Use stable, uneditableItemKeyvalues.BbScheduleradds day/week time slots, resource lanes, an event editor, recurrence and time zones. It is separate fromBbEventCalendar. BindEvents, useOnEventChangefor persistence, and setCancelto reject. Start/End are instants; an event's IANA time zone governs recurrence. Ical.Net 5.2.3 expands daily/weekly/monthly/yearly RRULEs, with occurrence exclusions and overrides. Timed events keep their elapsed duration across DST. The editor distinguishes repeated start/end times and rejects skipped times.BbTreeSelect<TItem>andBbCascader<TItem>accept nestedItemsplus key/text/children selectors. Bind stable string keys throughValue, orValuesin TreeSelect multiple mode.ValueExpression/ValuesExpressionintegrate withEditContext.BbFileUploadremains a file selector whenUploadHandleris null. With a handler, automatic uploads reportFileUploadItem.Status,BytesTransferredandProgress. Observe the cancellation token and report cumulative bytes throughFileUploadContext.ReportProgressAsync. Retry opens a fresh stream from byte zero. Retained hidden inputs preserve browser file handles across subsequent selections; removing/clearing files cancels their attempts.
Live demos and complete code examples are available at /components/datagrid-editing, /components/scheduler, /components/tree-select, /components/cascader and /components/file-upload.
Building release packages
Build and pack the current projects together by default; their project references keep the Components and Primitives APIs aligned. For the release workflow that switches to published package references, pass both -p:UsePackageReferences=true and -p:PrimitivesPackageVersion=<matching .NET 10 v4 release>. Publish that Primitives version first. The old hard-coded 4.0.0-beta.5 reference cannot supply the new editing APIs and has been removed. No packages are published by a local build or pack.
Need Help?
If you encounter any issues during migration:
- Check the GitHub Issues for known problems
- Join the GitHub Discussions to ask questions
- Review the Installation Guide for the latest setup instructions