Bridge helps shared Razor UI adapt to the host it is running in. Use the same components and services in Blazor WebAssembly, Blazor Server, and MAUI Blazor Hybrid to answer practical runtime questions:
- Am I running on the web or inside MAUI?
- Is this Android, iOS, Windows, Mac, or Linux?
- Should this layout behave like phone, tablet, or desktop?
- Is the app online?
- Is the user in light or dark mode?
- Does the screen need safe-area padding for a notch, cutout, or gesture area?
Bridge keeps those answers behind a small set of components and injectable services, so your shared UI does not need platform #if blocks.
Install the host package in the app that runs your UI:
| App type | Install |
|---|---|
| Blazor WebAssembly | Circuids.Bridge.Blazor |
| Blazor Server | Circuids.Bridge.Blazor |
| MAUI Blazor Hybrid | Circuids.Bridge.Maui |
| Shared Razor Class Library | Circuids.Bridge |
The Blazor and MAUI packages reference the core package transitively. Shared Razor Class Libraries normally reference only Circuids.Bridge; the host app provides the real implementation through DI.
Bridge setup has two parts: register the host implementation, then put a provider around the UI that should receive initialized Bridge state.
dotnet add package Circuids.Bridge.Blazorusing Circuids.Bridge.Blazor;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddBridgeForBlazor();
await builder.Build().RunAsync();@using Circuids.Bridge
<BridgeProvider>
@Body
</BridgeProvider>dotnet add package Circuids.Bridge.Blazorusing Circuids.Bridge.Blazor;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddBridgeForBlazor();
var app = builder.Build();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();@using Circuids.Bridge
<BridgeProvider>
@Body
</BridgeProvider>Bridge initializes after the interactive circuit is available. During static prerendering, components render with default values and update after the circuit connects.
dotnet add package Circuids.Bridge.Mauiusing Circuids.Bridge.Maui;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
builder.Services.AddMauiBlazorWebView();
builder.Services.AddBridgeForMaui();
return builder.Build();
}
}@using Circuids.Bridge
<BridgeProvider>
@Body
</BridgeProvider>Start with the components when the answer changes what you render.
<BridgeHost>
<Maui>
<NativeToolbar />
</Maui>
<Blazor>
<WebToolbar />
</Blazor>
<Default>
<StandardToolbar />
</Default>
</BridgeHost><BridgeFormFactor Context="viewport">
<DashboardFrame Layout="viewport.FormFactor" Width="viewport.Width" Height="viewport.Height" />
<Phone>
<MobileDashboard />
</Phone>
<TabletAndPhone>
<CompactDashboard />
</TabletAndPhone>
<Desktop>
<DesktopDashboard />
</Desktop>
<Default>
<LoadingLayout />
</Default>
</BridgeFormFactor>Plain child content receives the current component context. Named slots such as Phone, Desktop, Online, and Dark stay simple branch fragments and are used only for selecting what to render.
BridgeFormFactor uses this fallback order:
| Active form factor | First matching slot wins |
|---|---|
| Phone | Phone, TabletAndPhone, DesktopAndPhone, Default |
| Tablet | Tablet, TabletAndPhone, DesktopAndTablet, Default |
| Desktop | Desktop, DesktopAndTablet, DesktopAndPhone, Default |
| Unknown | Default |
<BridgeConnectivity>
<Online>
<SyncStatus />
</Online>
<Offline>
<OfflineBanner />
</Offline>
</BridgeConnectivity><BridgeTheme>
<Light><AppShell Theme="light" /></Light>
<Dark><AppShell Theme="dark" /></Dark>
<Default><AppShell Theme="system" /></Default>
</BridgeTheme>
<BridgeSafeArea Context="safeArea">
<div style="padding: @(safeArea.Top)px @(safeArea.Right)px @(safeArea.Bottom)px @(safeArea.Left)px">
<MainShell />
</div>
</BridgeSafeArea>For web safe-area insets, add viewport-fit=cover to the app host page:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">BridgeFormFactor and BridgeConnectivity can push their current state into fields with normal Blazor binding.
<BridgeFormFactor @bind-FormFactor="currentFormFactor">
<Desktop>
<DesktopDashboard />
</Desktop>
<Default>
<CompactDashboard />
</Default>
</BridgeFormFactor>
<BridgeConnectivity @bind-IsConnected="isConnected">
<Online>
<SyncStatus />
</Online>
<Offline>
<OfflineBanner />
</Offline>
</BridgeConnectivity>
<p>@currentFormFactor.FormFactor</p>
<p>@(isConnected ? "Online" : "Offline")</p>
@code {
private FormFactorInfo currentFormFactor = FormFactorInfo.Unknown();
private bool isConnected;
}Inject services when runtime state belongs in code rather than markup.
@inject IBridge Bridge
@inject IBridgeFormFactor FormFactor
@inject IBridgeConnectivity Connectivity
<p>@Bridge.Host on @Bridge.Platform</p>
<p>@FormFactor.FormFactor.FormFactor</p>
<p>@(Connectivity.IsConnected ? "Online" : "Offline")</p>For host-specific C# behavior, use a host handler. OnBlazor() is the required baseline; MAUI, WPF, and WinForms fall back to it unless you override them.
using Circuids.Bridge;
public sealed class StoragePathHandler : BridgeHostHandler<string>
{
public StoragePathHandler(IBridge bridge) : base(bridge)
{
}
protected override string OnBlazor() => "/local-storage";
protected override string OnMaui() => FileSystem.AppDataDirectory;
}Most apps can use the default provider:
<BridgeProvider>
@Body
</BridgeProvider>Tune behavior only when the app needs it.
<BridgeProvider
FormFactorResizeMode="ResizeMode.Global"
ConnectivityOptions='@(new ConnectivityOptions { IntervalInSeconds = 30, TestUrl = "/health" })'>
@Body
</BridgeProvider>| Option | When to use it |
|---|---|
ResizeMode.None |
Components create form-factor listeners only when needed. This is the default. |
ResizeMode.Global |
The app always needs responsive state and should keep one listener active. |
ResizeMode.Once |
The app needs the initial form factor only. |
ConnectivityOptions |
Blazor apps should poll a self-hosted URL such as /health or /favicon.ico. MAUI uses native connectivity and ignores this option. |
You can also initialize only one feature with an individual provider:
<BridgeThemeProvider>
@Body
</BridgeThemeProvider>Individual providers do not initialize IBridge. Use BridgeProvider when host/platform state is part of the same subtree.
| Need | Component | Service |
|---|---|---|
| Host-specific rendering | BridgeHost |
IBridge |
| Platform-specific rendering | BridgePlatform |
IBridge |
| Phone/tablet/desktop layout | BridgeFormFactor |
IBridgeFormFactor |
| Online/offline UI | BridgeConnectivity |
IBridgeConnectivity |
| Light/dark UI | BridgeTheme |
IBridgeTheme |
| Notch/cutout padding | BridgeSafeArea |
IBridgeSafeArea |
- Getting Started - setup details for each host.
- Usage Guide - complete component and service examples.
- API Reference - interfaces, enums, records, components, providers, and handlers.
Contributions are welcome. Please open an issue or submit a pull request with a clear description of the change and the behavior it affects. For significant changes, please discuss the approach in an issue before implementing it and also add or update tests as needed.
When contributing, please follow the existing code style and patterns. For new features, include unit tests for any new logic and consider adding component tests for any new rendering behavior. If the change affects public runtime behavior, add Pulse conformance cases to ensure it works correctly in real host apps.
- Architecture: docs/architecture.md
- Testing: docs/testing-architecture.md
Bridge is the production successor to MauiBlazorBridge, an earlier experimental package by the same author. That package is now archived. Bridge was written from the ground up as a stable, production-ready implementation of the same concept.
Bridge is licensed under the Apache-2.0 License.
If you find this project useful and would like to support its continued development, consider becoming a sponsor. Your contributions are instrumental in keeping this project maintained and growing. Thank you for your support.
