Koala logo Design

Settings sidebar

Vertical tab navigation for settings-style pages where the section list outgrew a horizontal tab strip. Renders as a 200px left rail at lg+ and a dropdown below that. Reuses the same TabNavModel and TabItem records as horizontal tabs, so a page can swap between layouts without rebuilding its tab data.

Active, inactive, and hover states

The active item uses brand-coloured text on a brand-soft tint so it stands out clearly on the cream page background in light mode. Hover gives inactive items a gray-100 wash. Active styling is driven by aria-current="page" so client-side switchTab() can flip the active item by toggling that attribute alone — no partial re-render.

Partner terms

Section content for the active tab.

Mobile dropdown

Below lg, the sidebar collapses to an Alpine.js dropdown. The trigger shows the active tab label; the panel lists all tabs and dismisses on outside click or escape. Resize this page to mobile to see it.

Usage

Build a TabNavModel with the section list, then drop the partial into a two-column grid alongside the swap target. The partial handles both the desktop rail and the mobile dropdown.

@{
    var settingsTabNav = new TabNavModel
    {
        TargetId = "settings-tabs",
        ActiveTab = Model.ActiveTab,
        PushHistory = false,
        Breakpoint = "lg",
        Tabs =
        [
            new TabItem { Key = "organisation",  Label = "Organisation",  Url = orgTabUrl,       Skeleton = "orgSettings" },
            new TabItem { Key = "discounts",     Label = "Discounts",     Url = discountsTabUrl, Skeleton = "table" },
            new TabItem { Key = "partner-terms", Label = "Partner terms", Url = termsTabUrl,     Skeleton = "termsList" },
            // ...
        ]
    };
}

<div data-tab-wrapper class="grid grid-cols-1 lg:grid-cols-[200px_1fr] gap-x-8 gap-y-6">
    <partial name="_SettingsSidebar" model="settingsTabNav" />
    <div id="settings-tabs">
        @switch (Model.ActiveTab)
        {
            case "organisation":  <partial name="_Organisation"  model="Model" /> break;
            case "discounts":     <partial name="_Discounts"     model="Model" /> break;
            case "partner-terms": <partial name="_PartnerTerms"  model="Model" /> break;
        }
    </div>
</div>

Key rules

  • Use the settings sidebar when there are 4+ sections — below that, prefer horizontal tabs
  • Tab keys map to ?tab=<key> querystring values; the page model selects the active tab from that
  • PushHistory = false — settings tab swaps shouldn't grow the back-button history
  • Breakpoint = "lg" — must be a literal lg, hardcoded in the partial; Tailwind can't see dynamically-interpolated breakpoint classes
  • The sidebar is sticky below the page header so it stays visible when long sections scroll
  • Active state is driven by aria-current="page" so switchTab() updates the active item without re-rendering the partial
  • Each TabItem sets a Skeleton string so the section pre-renders the right shape during AJAX swap (see skeletons)