Tab List

Accessible tabbed interface with keyboard navigation

Purpose of Component

RfTabList provides an accessible tabbed interface with keyboard navigation support. It manages the registration, activation, and navigation of tabs while ensuring only one tab is active at a time. Individual tabs are defined using RfTab components within the tab list.

When to Use

When you need to organize related content into separate panels that users can switch between, such as settings panels, different views of data, or step-by-step processes.

Basic Usage

To use RfTabList, create a tab list container and define individual tabs using RfTab components. Each tab has a unique identifier and contains both the tab header content and the panel content.

The RfTabList manages tab state through a RfTabListContext which is passed down to child tabs via a CascadingParameter.

RfTabList Component

The main container that manages all tabs and handles keyboard navigation.

Parameter Type Description
ActiveTabId string The identifier of the currently active tab. Required parameter.
ActiveTabIdChanged EventCallback<string> Callback invoked when the active tab ID changes.
TabListAriaLabel string ARIA label for the tab list element for accessibility.
CssClass string CSS classes to apply to the component wrapper.
TabsCssClass string CSS classes for the tabs container. Use Bulma classes like .is-boxed, .is-toggle, etc.
PanelCssClass string CSS classes for the panel wrapper element.
ChildContent RenderFragment Content containing RfTab components.

RfTab Component

Individual tab component that defines both the tab header and panel content.

Parameter Type Description
TabId string Unique identifier for the tab. Required parameter.
Tab RenderFragment Content to render in the tab header.
Panel RenderFragment Content to render in the tab panel.
LoadingPanel RenderFragment Content to display while the tab is loading.
ShowPanelWhenHidden bool Whether to render panel content even when not active (for SEO). Default: false.
TabCssClass string CSS classes to apply to the tab element.
PanelCssClass string CSS classes to apply to the panel element.
OnShow EventCallback<string> Callback invoked when the tab becomes visible.
OnLoad EventCallback<string> Callback invoked when the tab is loaded for the first time.
OnHide EventCallback<string> Callback invoked when the tab is hidden.

Keyboard Navigation

The tab list supports full keyboard navigation:

  • Arrow Left/Right: Navigate between tabs
  • Home: Focus first tab
  • End: Focus last tab

Basic Example

Below shows a basic example of setting up the RfTabList with multiple tabs.

<RfTabList @bind-ActiveTabId="ActiveTabId">
    <RfTab TabId="tab1">
        <Tab>First Tab</Tab>
        <Panel>
            <p>This is the content of the first tab.</p>
        </Panel>
    </RfTab>
    <RfTab TabId="tab2">
        <Tab>Second Tab</Tab>
        <Panel>
            <p>This is the content of the second tab.</p>
        </Panel>
    </RfTab>
    <RfTab TabId="loading-tab" OnLoad="LoadTabContent">
        <Tab>Loading Tab</Tab>
        <LoadingPanel>
            <p>Loading content, please wait...</p>
        </LoadingPanel>
        <Panel>
            <p>Content loaded successfully!</p>
        </Panel>
    </RfTab>
</RfTabList>
NOTE: Each tab must have a unique TabId within the tab list. The OnLoad callback is only called once when the tab is first activated.

Loading States

Tabs can display loading content using the LoadingPanel render fragment. During loading, the tab will show a loading indicator and call the OnLoad callback.

When OnLoad Fails

If your OnLoad callback throws, the exception still reaches the caller, but the tab does not stay stuck: the loading indicator is cleared and the tab is not marked as loaded, so activating it again runs OnLoad again. Use this to retry a failed load rather than leaving the user on a spinner.

Disposal-Safe Loading

An OnLoad callback is an async task owned by your page, not by RfTabList. The tab list shows the LoadingPanel until your callback returns, but it does not cancel the callback when the user navigates away mid-load: Blazor disposes the page while your await is still in flight, and the continuation keeps running. Calling StateHasChanged (or updating any state that triggers a re-render) at that point throws ObjectDisposedException and surfaces as an unhandled exception on the circuit.

The guard belongs in the hosting page: implement IDisposable, cancel a CancellationTokenSource in Dispose, pass its token to the awaited work, treat OperationCanceledException as a normal early return, and gate every post-load state update behind a disposed flag. The Loader Example and the Tab List playground implement exactly this pattern.

@implements IDisposable

@code {
    // Cancelled in Dispose so in-flight loads abort instead of resuming
    // onto a dead circuit.
    private CancellationTokenSource LoadCancellation { get; } = new();

    // True once Dispose has run; gates every post-load state update.
    private bool IsDisposed { get; set; }

    private void SetLoadedAt(string value)
    {
        if (IsDisposed)
        {
            // Navigated away mid-load: there is no renderer left to update.
            return;
        }

        LoadedAt = value;
        StateHasChanged();
    }

    private async Task LoadTabContent(string tabId)
    {
        try
        {
            await Task.Delay(3000, LoadCancellation.Token); // Or your real async fetch
        }
        catch (OperationCanceledException)
        {
            return; // Disposed mid-load: abandon the continuation.
        }

        SetLoadedAt(DateTime.Now.ToString("HH:mm:ss"));
    }

    public void Dispose()
    {
        IsDisposed = true;
        LoadCancellation.Cancel();
        LoadCancellation.Dispose();
    }
}

Async Context Events

Tabs and the tab list communicate through RfTabListContext, whose OnTabRegister event is async and multicast: each RfTab raises it while registering and the RfTabList subscribes to adopt the tab. Extra subscribers can attach to the same event.

Raising the event awaits every subscriber before it returns, so a tab is not considered registered until all handlers finish their tasks. Subscribers are invoked in registration order and run to completion. If exactly one subscriber faults, its exception is rethrown unchanged to the raiser; if more than one faults, the faults are combined into a single AggregateException. A subscriber that throws before its first await does not stop the others from running.

Styling with Bulma

The tab list integrates with Bulma CSS framework classes:

  • .is-boxed - Boxed tabs
  • .is-toggle - Toggle-style tabs
  • .is-fullwidth - Full width tabs
  • .is-centered - Centered tabs
  • .is-right - Right-aligned tabs

How to Setup

Setting up RForge is simple. Follow the below steps to add RForge Blazor components to your project.

  1. Install the Nuget package
  2. Import the namespaces (optional)
  3. Register RForge services
  4. Include Bulma CSS

Install the Nuget Package

RForge Blazor components are installed via Nuget. There are two packages. One for the blazor app and one for the library if any.

Blazor Project Installation

Command Line
dotnet add package RForge.Blazor
Package Manager Console
Install-Package RForge.Blazor

Library Project Installation

This is optional but may be useful if you want to use common enums across the backend and frontend.

Command Line
dotnet add package RForge.Abstractions
Package Manager Console
Install-Package RForge.Abstractions

Import the namespaces

To simplify namespacing while using RForge, the package is designed with most all components sitting within the root RForge namespace.

You may want to include the following namespaces in your _import.razor for ease of use.

@using RForgeBlazor
@using RForgeBlazor.Services
@using RForge.Abstractions
Namespace Purpose
RForgeBlaozr Houses all of the blazor components.
RForgeBlaozr.Services Houses interfaces you may need to communicate with certain components.
RForgeBlaozr.Abstractions Common enums used through out the components.

Register RForge Services

In order for some components to work they must first have their services registered.

Open up Program.cs and add the following line before the var app = builder.Build();.

Heads Up!
If your project is using the multi project solution where one project is server and the other is client. You will need to register the services in both projects.

using RForgeBlazor;
//...
builder.Services.AddRfForgeBlazorServices();
//...
var app = builder.Build();
    

Include Bulma CSS

RForge makes use of Bulma CSS to stylize the components. Add / install Bulma and add the CSS file to the head. Link to download: Bulma releases.

An unhandled error has occurred. Reload 🗙