Skip to content

fix: programmatic tab selection flips between tabs in nested FluentTabs (regression 5.0.0 -> 5.0.1) #5468

Open

Description

@victorburckel

🐛 Bug Report

After upgrading from 5.0.0 to 5.0.1, selecting a tab from code (@bind-ActiveTabId or SelectTabAsync) in a nested FluentTabs makes the active tab flip back and forth between two the two tabs indefinitely.

💻 Repro or Code Sample

Blazor Server (InteractiveServer), net10.0. Steps: click Add session, then Show results.

Home.razor:

@page "/"
@rendermode InteractiveServer

<FluentButton OnClick="AddSession">Add session</FluentButton>
<FluentButton OnClick="ShowResults">Show results</FluentButton>

@if (_sessions.Count > 0)
{
    <FluentTabs @bind-ActiveTabId="_activeTabId">
        @foreach (var session in _sessions)
        {
            <FluentTab Id="@($"tab-{session.Id}")" Header="@($"Session {session.Id}")">
                <FluentTabs @bind-ActiveTabId="session.ActiveInnerTabId">
                    <FluentTab Id="@($"setup-{session.Id}")" Header="Setup">Setup content</FluentTab>
                    <FluentTab Id="@($"results-{session.Id}")" Header="Results" Visible="@session.HasResults">
                        <FluentTabs>
                            <FluentTab Header="Canonicals">Canonicals content</FluentTab>
                            <FluentTab Header="Greeks">Greeks content</FluentTab>
                        </FluentTabs>
                    </FluentTab>
                </FluentTabs>
            </FluentTab>
        }
    </FluentTabs>
}

@code {
    private readonly List<Session> _sessions = [];
    private string? _activeTabId;

    private void AddSession()
    {
        var session = new Session(_sessions.Count + 1);
        _sessions.Add(session);
        _activeTabId = $"tab-{session.Id}";
    }

    private async Task ShowResults()
    {
        var session = _sessions[^1];
        await Task.Run(() => Thread.Sleep(500)).ConfigureAwait(false);
        session.HasResults = true;
        await InvokeAsync(StateHasChanged).ConfigureAwait(false);
        session.ActiveInnerTabId = $"results-{session.Id}";
        await InvokeAsync(StateHasChanged).ConfigureAwait(false);
    }

    private sealed class Session(int id)
    {
        public int Id { get; } = id;

        public bool HasResults { get; set; }

        public string ActiveInnerTabId { get; set; } = $"setup-{id}";
    }
}

🤔 Expected Behavior

The inner tabs select Results and stay there (this is the behavior of 5.0.0).

😯 Current Behavior

The activeid of the inner fluent-tablist alternates between results and setup indefinitely.

Each server render writes activeid, the client immediately raises tabchange with that same value, and the handler then sets the opposite value.

  • Same repro on 5.0.0: stable on Results.
  • Using SelectTabAsync through @ref instead of the binding behaves the same.
  • Not selecting any tab from code (user clicks only) works fine, as does the outer (non-nested) tab selection.

💁 Possible Solution

It looks related to #5381 / #5382 ("Tabs: Simplify active tab ID handling"), which removed the _clientActiveTabId guard. OnParametersSet now always writes ActiveTabId back to the activeid attribute of fluent-tablist, which raises tabchange, which updates the bound value and re-renders.

🔦 Context

We upgraded Fluent UI from 5.0.0 to 5.0.1. Our pricing page selects the Results tab automatically once pricing is done. With 5.0.1 the tabs flip endlessly. As a workaround we no longer select the tab from code, so users have to click Results themselves.

🌍 Your Environment

  • OS & Device: Windows on PC
  • Browser: Chromium (Playwright)
  • .NET and Fluent UI Blazor library Version: .NET 10 and 5.0.1 (works on 5.0.0)

Activity

  1. Nacl-HCl commented on Oct 28, 2026

    @Nacl-HCl

    Hi Denis Voituron (@dvoituron), quick question on the new Issue discussion declaration check for #5469: it asks the PR description to confirm the change was discussed in an issue with maintainer agreement before implementation, or that a documented exception applies.

    To be transparent, this PR went up about two hours after #5468 was filed, with no discussion in the issue beforehand — so I can't honestly tick that box as worded. The change has since been discussed at length here in the PR review.

    Would you consider that review discussion sufficient, or is there an exception you'd point me to? I'll follow whatever you advise — I just don't want to tick a box that doesn't match what happened.

  2. dvoituron commented on Oct 28, 2026

    @dvoituron
    Collaborator

    This new checkbox is primarily intended to prevent the creation of pull requests that add “unnecessary” new features. This requires work on the part of the pull request creator and on our part to discuss and verify the feature.
    In this case, it’s more of a bug that needs to be fixed.

    The best practice is therefore to check this box, as described in this paragraph.
    https://github.com/microsoft/fluentui-blazor/blob/dev/docs/contributing.md#discuss-before-implementing

    Concerning this PR, I find it complicated: is it possible to avoid adding variables like _pendingAttributeRewrite and _pendingRewriteBase, which will quickly make the code difficult to maintain (no idea if this is possible)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions