This guide covers all Bridge features with practical examples. All components and interfaces are in the Circuids.Bridge namespace.
@using Circuids.Bridge- Host Detection
- Platform Detection
- Form Factor Detection
- Connectivity Monitoring
- Theme Detection
- Safe Area Insets
- BridgeHostHandler — Host-Specific Logic in C#
- Two-Way Binding
- Using Services Directly via DI
Detect whether the app is running in MAUI, Blazor, WPF, or WinForms.
<BridgeHost>
<Maui>
<p>Running inside MAUI Blazor Hybrid</p>
</Maui>
<Blazor>
<p>Running in the browser (Blazor WASM or Server)</p>
</Blazor>
<Default>
<p>Unknown host environment</p>
</Default>
</BridgeHost><BridgeHost>
@if (context == Host.Maui)
{
<MauiSpecificComponent />
}
else
{
<WebSpecificComponent />
}
</BridgeHost>@inject IBridge Bridge
<p>Host: @Bridge.Host</p>Detect the operating system: Android, iOS, Windows, Mac, or Linux.
<BridgePlatform>
<Android>Android device</Android>
<IOS>iPhone or iPad</IOS>
<Windows>Windows desktop</Windows>
<Mac>macOS</Mac>
<Linux>Linux</Linux>
<Default>Unknown platform</Default>
</BridgePlatform>@inject IBridge Bridge
<p>Platform: @Bridge.Platform</p>
<p>Version: @Bridge.PlatformVersion</p>The PlatformChanged event fires once the platform is detected (useful when the provider initializes after the component renders):
@inject IBridge Bridge
@implements IDisposable
<p>Platform: @Bridge.Platform</p>
@code {
protected override void OnInitialized()
{
Bridge.PlatformChanged += OnPlatformChanged;
}
private void OnPlatformChanged(object? sender, PlatformIdentity e)
{
InvokeAsync(StateHasChanged);
}
public void Dispose() => Bridge.PlatformChanged -= OnPlatformChanged;
}Classify the device as Phone, Tablet, or Desktop based on viewport width.
| Form Factor | Width Range |
|---|---|
| Phone | ≤ 767px |
| Tablet | 768px – 1023px |
| Desktop | ≥ 1024px |
<BridgeFormFactor>
<Phone>
<MobileLayout />
</Phone>
<Tablet>
<TabletLayout />
</Tablet>
<Desktop>
<DesktopLayout />
</Desktop>
<Default>
<p>Loading...</p>
</Default>
</BridgeFormFactor>Target multiple form factors with a single fragment:
<BridgeFormFactor>
<Desktop>
<SidebarLayout />
</Desktop>
<TabletAndPhone>
<StackedLayout />
</TabletAndPhone>
</BridgeFormFactor>Available combinations: DesktopAndTablet, DesktopAndPhone, TabletAndPhone.
Resolution order:
- Phone:
Phone→TabletAndPhone→DesktopAndPhone→Default - Tablet:
Tablet→TabletAndPhone→DesktopAndTablet→Default - Desktop:
Desktop→DesktopAndTablet→DesktopAndPhone→Default
<BridgeFormFactor>
@{
var info = context;
}
<p>Form factor: @info.FormFactor</p>
<p>Viewport: @info.Width × @info.Height</p>
</BridgeFormFactor>For static layouts that don't need to respond to resizes:
<BridgeFormFactor ListenOnce="true">
<Phone><MobileView /></Phone>
<Desktop><DesktopView /></Desktop>
</BridgeFormFactor>@inject IBridgeFormFactor FormFactor
<p>Current: @FormFactor.FormFactor.FormFactor (@FormFactor.FormFactor.Width × @FormFactor.FormFactor.Height)</p>Monitor internet connectivity in real-time.
<BridgeConnectivity>
<Online>
<p>You're connected to the internet.</p>
</Online>
<Offline>
<div class="alert alert-warning">
No internet connection. Some features may be unavailable.
</div>
</Offline>
</BridgeConnectivity><BridgeConnectivity>
<p>Online: @context</p>
</BridgeConnectivity>@inject IBridgeConnectivity Connectivity
@implements IDisposable
<p>Connected: @Connectivity.IsConnected</p>
@code {
protected override void OnInitialized()
{
Connectivity.ConnectionChanged += OnConnectionChanged;
}
private void OnConnectionChanged(object? sender, bool isConnected)
{
InvokeAsync(StateHasChanged);
}
public void Dispose() => Connectivity.ConnectionChanged -= OnConnectionChanged;
}On Blazor, connectivity is checked by polling a URL. Configure this via ConnectivityOptions:
<BridgeProvider ConnectivityOptions="@(new ConnectivityOptions
{
IntervalInSeconds = 30,
TestUrl = "/api/health"
})">
@Body
</BridgeProvider>| Property | Default | Description |
|---|---|---|
IntervalInSeconds |
10 |
Polling interval in seconds. |
TestUrl |
"/favicon.ico" |
URL to HEAD-request. Use a self-hosted endpoint to avoid CORS and regional blocks. |
On MAUI, connectivity uses the native
Connectivity.ConnectivityChangedevent — no polling needed.ConnectivityOptionsis ignored.
Detect the system light/dark mode preference.
<BridgeTheme>
<Light>
<div class="light-theme">Light mode content</div>
</Light>
<Dark>
<div class="dark-theme">Dark mode content</div>
</Dark>
<Default>
<div>Theme not detected</div>
</Default>
</BridgeTheme><BridgeTheme>
<p>Current theme: @context</p>
</BridgeTheme>@inject IBridgeTheme Theme
@implements IDisposable
<p>Theme: @Theme.Theme</p>
@code {
protected override void OnInitialized()
{
Theme.ThemeChanged += OnThemeChanged;
}
private void OnThemeChanged(object? sender, ThemeMode mode)
{
InvokeAsync(StateHasChanged);
}
public void Dispose() => Theme.ThemeChanged -= OnThemeChanged;
}Get safe area insets for notched/cutout devices (iPhone notch, Android camera cutout, gesture navigation bars).
<BridgeSafeArea>
@{
var insets = context;
}
<div style="padding: @(insets.Top)px @(insets.Right)px @(insets.Bottom)px @(insets.Left)px;">
<p>Content with safe area padding</p>
</div>
</BridgeSafeArea>@inject IBridgeSafeArea SafeArea
@if (SafeArea.SafeArea.HasInsets)
{
<div style="padding-top: @(SafeArea.SafeArea.Top)px; padding-bottom: @(SafeArea.SafeArea.Bottom)px;">
@ChildContent
</div>
}For safe area insets to work in Blazor, add viewport-fit=cover to your HTML:
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">Execute different C# logic depending on the host — without #if preprocessor directives.
using Circuids.Bridge;
public class StoragePathHandler(IBridge bridge) : BridgeHostHandler<string>(bridge)
{
protected override string OnBlazor() => "/local-storage";
protected override string OnMaui() => FileSystem.AppDataDirectory;
}
// Usage
var handler = new StoragePathHandler(bridge);
string path = handler.Execute();public class NotificationHandler(IBridge bridge) : BridgeHostHandler(bridge)
{
protected override void OnBlazor()
{
// Show browser notification via JS interop
}
protected override void OnMaui()
{
// Show native MAUI notification
}
}public class DataSyncHandler(IBridge bridge) : BridgeHostHandlerAsync(bridge)
{
protected override async Task OnBlazor()
{
await SyncWithIndexedDb();
}
protected override async Task OnMaui()
{
await SyncWithSqlite();
}
}
// Usage
await handler.ExecuteAsync();OnMaui(), OnWpf(), and OnWinForms() default to calling OnBlazor(). Override only the hosts that need different behavior:
public class ClipboardHandler(IBridge bridge) : BridgeHostHandler(bridge)
{
protected override void OnBlazor() => /* JS clipboard API */;
protected override void OnMaui() => /* MAUI clipboard */;
protected override void OnWpf() => /* WPF-specific clipboard */;
// OnWinForms() falls back to OnBlazor() by default
}The BridgeFormFactor and BridgeConnectivity components support two-way binding:
<BridgeFormFactor @bind-FormFactor="currentFormFactor">
<Phone>Phone UI</Phone>
<Desktop>Desktop UI</Desktop>
</BridgeFormFactor>
<p>Current: @currentFormFactor.FormFactor</p>
@code {
private FormFactorInfo currentFormFactor = FormFactorInfo.Unknown();
}<BridgeConnectivity @bind-IsConnected="isOnline">
<Online>Online</Online>
<Offline>Offline</Offline>
</BridgeConnectivity>
@code {
private bool isOnline;
}All features are available as injectable services — you don't have to use the Razor components.
@inject IBridge Bridge
@inject IBridgeFormFactor FormFactor
@inject IBridgeConnectivity Connectivity
@inject IBridgeTheme Theme
@inject IBridgeSafeArea SafeArea| Service | Key Properties/Events |
|---|---|
IBridge |
Host, Platform, PlatformVersion, IsInitialized, PlatformChanged |
IBridgeFormFactor |
FormFactor, FormFactorChanged, CreateListenerAsync(), DisposeListenerAsync() |
IBridgeConnectivity |
IsConnected, ConnectionChanged |
IBridgeTheme |
Theme, ThemeChanged |
IBridgeSafeArea |
SafeArea, SafeAreaChanged |
Important: Services must be initialized before use. The
<BridgeProvider>(or individual providers) handles this automatically. If you bypass providers, callInitializeAsync()manually.