Crossing Fluent UI Blazor 4 → 5

The portal's Blazor front end renders the framework's controls through Fluent UI Blazor (Microsoft.FluentUI.AspNetCore.Components and .Icons, pinned in src/Directory.Packages.Plugins.props — core carries no GUI technology, policy core-carries-no-gui-technology). Version 5 is not an upgrade of version 4: it is a rewrite on the Fluent 2 web components, with its own component names, enums, dialog and theme systems and CSS tokens. This page is the record of the crossing, in the shape of core's Dependency Major Upgrades ledger, and the reference for what the platform now carries itself.

It is also a step in the direction The design system sets: away from Fluent's look, onto the platform's --mw-* tokens. Nothing here reverses that direction; where v5 removed a component the portal relied on, the replacement is the documented v5 one, or a platform piece that already existed.

Ledger entry

Microsoft.FluentUI.AspNetCore.Components 4.14.4 → 5.0.1 (and .Icons 4.14.4 → 5.0.1)

Step Result
Registry …/v3-flatcontainer/microsoft.fluentui.aspnetcore.components/index.json: newest stable 5.0.1 (after 5.0.0 and five release candidates); .icons: newest stable 5.0.1. Both .nupkgs downloaded and extracted.
Licence MIT at both ends — <license type="expression">MIT</license> in the 4.14.4 and 5.0.1 nuspecs of both packages. No change.
Target frameworks 5.0.1 ships net8.0, net9.0, net10.0, net11.0. The crossing rides the platform-4 line (net11.0, MeshWeaver.Plugins#5421), so the portal binds the net11.0 asset; nothing in v5 itself required net11 — the move was first built and tested on net10.0 as well.
Dependency floor The net11.0 group requires Microsoft.AspNetCore.Components.Web >= 11.0.0-rc.1 (the net10.0 group >= 10.0.12). The platform-4 line already pins Components.Web at 11.0.0, so no pin moves.
Public API MetadataLoadContext over the net10.0 assets: 603 → 539 public types; 400 type names removed (generic-arity renames included — FluentDatePicker → FluentDatePicker<TValue>, the list components' second type parameter), 336 added, 203 kept; 4,140 public member lines removed. 61 removed type names were referenced by this repository's src/ (by name; the count includes common words such as Option that also name non-Fluent types).
Library self-check No EnablePackageValidation baseline and no CompatibilitySuppressions in 5.0.1 — the maintainers ship a migration guide (Documentation/GetStarted/Migration/*.md, 60 pages) instead, which this crossing followed component by component. No analyzers ship in either package.
Obsolete members v5 keeps the v4 enums that the guide maps (Appearance, FluentInputAppearance, TooltipPosition, …) in a Migration folder with every member [Obsolete], so -warnaserror (CS0618) refuses each use. None is used any more; the conversions live in platform code that names v4's WIRE names (below).
CSS tokens v4's FluentDesignTheme computed --neutral-*, --accent-*, --type-ramp-*, … in JavaScript; v5 writes the Fluent 2 tokens (--colorNeutralForeground1, --colorBrandBackground, …) instead. Measured across this repository and core: 1,790 reads of 61 distinct v4 names. Core's DesignTokens.FluentAliases already defines 62 of them; 32 names (105 reads) were defined by nothing on v5 — see The CSS tokens below.
In-mesh source No NodeType source, script or test in a module folder references a Fluent type (git grep Microsoft.FluentUI outside src/: none). In-mesh layout areas reach the renderer through the UiControl records, whose wire vocabulary (appearance names, typographies) the views now translate explicitly. Their CSS strings read v4 token NAMES — which is why those names stay defined.
Behaviour the compiler did not see The suites found four, each a runtime throw or a markup change behind a compiling call: every v5 input REFUSES a null Id (FluentInputBase.SetParametersAsync throws InvalidOperationException, where v4 minted its own — FormComponentBase.InputId now mints one); FluentRadio must carry its group's TValue (an untyped radio throws); every v5 input is wrapped in a <fluent-field> (label + input + message), which is now the box a dialog's full-width rule must reach; and the date picker renders its calendar into the markup, day buttons and their aria-labels included. The accessible name goes through the inputs' own AriaLabel parameter, which each v5 input routes to its own element.
Builds Every project that references Fluent or a Blazor project built -c Release -warnaserror, 0 Error(s) — against core main (net10.0) and again against the platform-4 tree (net11.0), both portal hosts included.
Suites 9 GUI suites on net11.0, 3,120 tests, 0 failed — see Suites below.

The mappings applied

The migration guide's tables, applied as written; the left column is v4, the right what the portal renders now.

v4 v5 Where
FluentDesignTheme StorageName="theme" (mode + Office accent) ThemeSync → ThemeStore (platform): the same "theme" key and JSON shape, plus IThemeService.SetThemeAsync for v5's own tokens App, pages, top bar, public layout, Settings → Appearance
DesignThemeModes (System = 0, Light, Dark) ThemeMode (Light = 0, Dark, System) — JS callers now receive the NAME, never the ordinal cascaded ThemeMode, Monaco, Mermaid, Markdown
OfficeColor / OfficeColorUtilities AccentPresets (platform; v4's 25 names and hex values unchanged) Appearance picker, design-token stylesheet
FluentDialogProvider, FluentToastProvider, FluentTooltipProvider, FluentKeyCodeProvider, FluentMenuProvider FluentProviders (menus need no provider; toast count and lifetime are library configuration) AreaPage, PortalLayoutBase, PublicLayout
AddFluentUIComponents() AddFluentUIComponents(FluentLibraryDefaults.Apply) — restores v4's 10px FluentStack gaps and FluentGrid Spacing=3, sets 3 toasts / 5 s hosting and every bUnit test
FluentButton Appearance=Appearance.* ButtonAppearance: Accent → Primary, Neutral/Hypertext/Filled → Default, Lightweight → Transparent, Outline → Outline, Stealth → Subtle (MwAppearanceExtensions.ToButtonAppearance for wire names) every button, MenuItemView
FluentAnchor (button-styled) FluentAnchorButton logo, header links, sign-in, not-found page
FluentAnchor (hypertext) FluentLink; Target → LinkTarget; rel="noopener noreferrer" → implicit noopener on _blank + ReferrerPolicy=NoReferrer (LinkTargets) — FluentLink has no free rel LinkView, file-browser crumbs
FluentBreadcrumb / FluentBreadcrumbItem FluentStack of FluentLinks with chevron icons (the guide's documented replacement) FileBrowser
FluentToolbar FluentStack (horizontal, wrapping, 8px gap) ToolbarView, FileBrowser
FluentHeader / FluentFooter / FluentBodyContent FluentLayoutItem Area=Header/Footer/Content skins, portal header
FluentLabel Typo=… FluentText As=… Size=… (LabelTypography: v4's tag and type-ramp step per typography) Label view, every page label
FluentTextField / FluentSearch FluentTextInput (search icon in StartTemplate; Size in characters → Width in ch) text fields, mobile menu search
FluentNumberField FluentNumberInput dialogs, tests
FluentTextArea Rows/Cols Height (rows × 20px + 12px) / Width (ch) TextAreaView
FluentProgressRing FluentSpinner (style widths → SpinnerSize) 26 sites
FluentProgress Stroke/Paused FluentProgressBar Thickness (Paused gone: a Value means still) ProgressView, file upload
FluentSelect/Combobox/Listbox (SelectedOption) two type parameters, @bind-Value, OptionValueToString list views, editors, onboarding
FluentDatePicker FluentDatePicker<DateTime?>, CalendarDayFormat DateTimeView
FluentPersona FluentAvatar profile, image upload
FluentProfileMenu FluentButton + FluentPopover + FluentAvatar (the guide's documented replacement) UserProfile
FluentCounterBadge wrapping the bell FluentCounterBadge with AnchorContent, OverflowCount=99 NotificationCenter
FluentBadge Appearance/Color(string)/Circular/Fill/Dismiss* BadgeAppearance + BadgeColor (BadgeLooks), Shape; CSS colour/size as style BadgeView, catalog
FluentMessageBar Intent=MessageIntent.* MessageBarIntent.* message bars
FluentMenu Anchor/Open/OpenChanged FluentMenu Trigger= + FluentMenuList; open state owned by the web component top-bar menus
FluentMenu Open Anchored=false (inline panel) standalone FluentMenuList, rendered while open mobile menu
FluentDialog + FluentDialogHeader/Footer (inline) FluentDialog + FluentDialogBody TitleTemplate/ActionTemplate, opened with ShowAsync DialogView, camera sheet
IDialogService.ShowDialogAsync<T,TData>(data, DialogParameters) + IDialogContentComponent<T> ShowDialogAsync<T>(options ⇒ …Parameters…), cascaded IDialogInstance file dialogs, node preview
ShowPanelAsync ShowDrawerAsync (DialogAlignment.End) notification centre
ShowMessageBoxAsync(MessageBoxContent) ShowConfirmationAsync / ShowErrorAsync (message is plain text the dialog encodes) recycle, projection, error modal
IToastService.ShowSuccess/Error/Warning INotificationService.Show…ToastAsync through ToastNotifications (the v5 task completes when the toast CLOSES, so it is observed, never awaited) file browser, edit form
FluentAccordionItem HeadingTemplate HeaderTemplate expander, catalog
FluentGridItem xs/sm/… Xs/Sm/… catalog page
GenerateHeaderOption, Align, SortDirection DataGridGeneratedHeaderType, DataGridCellAlignment, DataGridSortDirection DataGridView
FluentSwitch Checked/UncheckedMessage removed (no v5 equivalent) switch views
FluentRadio child content Label radio groups
Id="@InputId" with a null id (v4 minted one) InputId is never null — the view mints mw-input-… once every form view
raw aria-label on inputs the inputs' AriaLabel parameter every form view
FluentRadio Value=@object FluentRadio TValue="TValue" RadioGroupView
JSModule (v4 public base class) the accessor holds its own module CacheStorageAccessor
fluent-text-field, fluent-search, fluent-number-field, fluent-text-area, fluent-progress-ring, fluent-anchor, fluent-select/fluent-combobox (CSS/JS tag selectors) fluent-text-input, fluent-textarea, fluent-spinner, fluent-anchor-button, fluent-dropdown; [appearance=stealth/accent/lightweight] → subtle/primary/transparent stylesheets, ButtonView.razor.js, thumbnail generator

SiteSettingsPanel (a v4 demo panel nothing referenced) was deleted rather than ported; Settings → Appearance (AppearanceView) is the live surface for the same choices.

What the platform now carries itself

The CSS tokens

Three blocks keep every rendered colour on the platform tokens (/_mw/design-tokens.css, Memex.Portal.Gui/Theme):

  1. core's DesignTokens.FluentAliases — v4's colour names onto --mw-* (unchanged);
  2. FluentV5TokenAliases — v5's Fluent 2 names onto --mw-*, applied like core's block (!important, on the root and on every element with an inline custom property), so v5's own theme script cannot win over the visitor's accent;
  3. FluentV4TokenRemainder — the 32 v4 names nothing else defines any more: shape and type-ramp tokens at v4's own default values, the remaining colours on --mw-*. They are DEFINED rather than rewritten because in-mesh C# and authored content read them and no compiler sees those reads.

Suites

Suite net10.0, first run net10.0, final net11.0 (platform-4 line)
MeshWeaver.Blazor.Views.Test 868 total, 34 failed 868, 0 failed 877, 0 failed
MeshWeaver.Blazor.EntityViews.Test 88 total, 30 failed 88, 0 failed 88, 0 failed
Memex.Portal.Gui.Test 1,072, 0 failed 1,072, 0 failed 1,072, 0 failed
MeshWeaver.Blazor.Chat.Test 155, 0 failed 155, 0 failed 155, 0 failed
MeshWeaver.Blazor.Portal.Test 216, 0 failed 216, 0 failed 216, 0 failed
MeshWeaver.Graph.Views.Test 369, 0 failed — 369, 0 failed
MeshWeaver.Hosting.Blazor.Test 276, 0 failed — 276, 0 failed
MeshWeaver.Blazor.Graph.Test 41, 0 failed — 41, 0 failed
MeshWeaver.Blazor.Analysis.Test 26, 0 failed — 26, 0 failed

Run as the xUnit v3 executables (bin/Release/<tfm>/<Project>), each built -c Release -warnaserror. The 64 first-pass failures were the crossing's real findings, not flakes: most were the null-Id refusal; then the untyped FluentRadio, the <fluent-field> wrapper the dialog full-width rule missed, a module-level listener registry in the new theme JS (ComponentJsIsInstanceScopedGuard), and a static renderer without v5's LibraryConfiguration / IJSRuntime; the rest asserted v4 markup (<fluent-anchor, rel="noopener noreferrer", the message bar's intent-* class, fluent-toolbar, the card's class, [appearance="accent"]) and now assert the v5 markup that carries the same property.

CI: the platform image still ships Fluent 4

A local build restores v5 and is green; CI is not, because the shared lane compiles the repository INSIDE the pinned platform image (build-project --bind-to-image, image memex-portal-ai), and that image ships Microsoft.FluentUI.AspNetCore.Components 4.14.4. The global workspace build fails MeshWeaver.Blazor with RZ10012/RZ9996 (FluentSpinner, FluentText, TitleTemplate, …) — v5 names compiled against the v4 assembly.

The lane's superseded-image-assemblies input is not a bridge for this as it stands, measured against core's lane and tester (node-repo-module-pack.yml, tools/MeshWeaver.PluginTester):

  1. It is refused as stale before anything compiles. SupersededEntryStaleness (MeshWeaver#3223) refuses an entry when the image's copy defines none of the fully-qualified type names this repository's source declares. The input exists for a type that MOVED out of an image assembly into this repository's source; no file here declares a Microsoft.FluentUI.AspNetCore.Components.* type, so the overlap is empty and the tester throws --superseded-image-assembly '…': STALE.
  2. Nothing would supply v5 in its place. The container build has no NuGet restore: beyond the image it binds only the module-libraries shelf (src/MeshWeaver.ModuleLibraries), which carries no Fluent package. Dropping v4 from the reference set alone would turn every Fluent use into CS0246.

So the crossing lands when the image the lane binds carries Fluent 5 — or the lane grows a declared, version-scoped "this repository rides a newer package than the image" entry (with the package on the shelf), which is a change to core's lane and tester, not to this repository.

What was not verified

No browser was driven. Every visual statement on this page is a statement about markup and CSS, not about pixels. In particular, unverified in a running portal: