Koala logo Design

Tabs

Tab navigation pattern with count pills, skeleton loading, and period filters. On mobile, tabs collapse into a rich-trigger dropdown styled like the sidebar partner / organisation switcher: icon for the active tab, "Section" label above the bold tab name, dual chevrons. On desktop, tabs use a horizontal bar with Alpine-AJAX for partial page updates.

Mobile tab dropdown (rich trigger)

On mobile, tabs render as a single Alpine dropdown that mirrors the sidebar switcher trigger: a small circular icon for the active tab, a tiny "Section" label above the bold tab name, and dual chevrons on the right. The panel lists every tab with its icon. Resize to mobile to see it.

Details tab content goes here.

Activity tab content goes here.

Notes tab content goes here.

@{
    var quoteTabNav = new TabNavModel
    {
        TargetId = "quote-tabs",
        ActiveTab = Model.ActiveTab,
        Tabs =
        [
            new TabItem { Key = "details",  Label = "General",  Url = detailsTabUrl,  Skeleton = "form",     Icon = IconName.FileText },
            new TabItem { Key = "notes",    Label = "Notes",    Url = notesTabUrl,    Skeleton = "notes",    Pill = Model.NoteCount,    Icon = IconName.MessageSquare },
            new TabItem { Key = "activity", Label = "Activity", Url = activityTabUrl, Skeleton = "timeline", Pill = Model.ActivityCount, Icon = IconName.Activity },
        ]
    };
}

<div data-tab-wrapper>
    <partial name="_TabNav" model="quoteTabNav" />
    <div id="quote-tabs"><!-- tab content --></div>
</div>

<!-- _TabNav renders both layouts:
     - Mobile (< bp): rich trigger via _TabNavMobileDropdown (icon + "Section" label
       + bold tab name + dual chevrons), reusing the shared partial so settings
       sidebars match exactly.
     - Desktop (>= bp): horizontal tab bar with optional koala-tab-pill counts. -->

Tab count pill

Use koala-tab-pill for count badges inside tab labels. The tag helper applies consistent sizing and colours.

Quotes 42 Transactions 18 Partners 7
<span koala-tab-pill>42</span>

<!-- Output classes -->
inline-flex items-center justify-center px-2 py-0.5 text-xs
font-medium rounded-full bg-gray-100 text-gray-600
dark:bg-gray-700 dark:text-gray-300

Skeleton loading

When a tab is clicked, a skeleton placeholder is shown via showTabSkeleton(tabsId, type) (defined in _Layout.cshtml). The active tab triggers the skeleton immediately, and the AJAX response replaces it.

<!-- Trigger skeleton on tab click -->
<a href="/partner/quotes/view/abc?tab=activity"
   x-target.push="main"
   x-on:click="@(Model.ActiveTab == "activity"
       ? "$event.preventDefault()"
       : "showTabSkeleton('tab-container', 'timeline')")">
    Activity
</a>

<!-- Available skeleton types -->
showTabSkeleton('container-id', 'table')     // Cards on mobile, header + rows on desktop
showTabSkeleton('container-id', 'form')      // 1-col mobile, 2-col label/value on desktop
showTabSkeleton('container-id', 'timeline')  // Filter pill + date header + avatar entries
showTabSkeleton('container-id', 'chart')     // Vertical bar chart placeholder
showTabSkeleton('container-id', 'notes')     // Add button + note cards with avatar + text
showTabSkeleton('container-id', 'users')     // Cards on mobile, avatar table rows on desktop

Skeleton type reference

Each skeleton type matches the content the tab displays.

Type Used for Description
table Quotes, Transactions, Partners Cards on mobile (< XL), header + rows on desktop
form Details, Settings 1-col on mobile, 2-col label/value grid on desktop
timeline Activity Filter pill + date header + avatar entries
chart Fees Vertical bar chart placeholder
notes Notes Add button + note cards with avatar + text
users Team Cards on mobile (< SM), avatar table rows on desktop

Period filter buttons

Pill-style buttons for filtering by time period. Default to 30 days. Standard options: 7 days, 30 days, 3 months, 12 months, All time.

<!-- Active state -->
class="bg-secondary text-white dark:text-gray-900
       rounded-lg px-3 py-1.5 text-sm font-medium"

<!-- Inactive state -->
class="text-gray-500 hover:bg-gray-100 dark:text-gray-400
       dark:hover:bg-gray-700 rounded-lg px-3 py-1.5 text-sm"

<!-- With Alpine-AJAX (server-driven) -->
<a href="/partner/quotes?period=7d"
   x-target.push="main"
   class="@(Model.Period == "7d"
       ? "bg-secondary text-white dark:text-gray-900"
       : "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700")
   rounded-lg px-3 py-1.5 text-sm font-medium">
    7d
</a>

Tab → content gap

The standalone _TabNav partial renders a 24px (mb-6) gap below the tab strip. Every tab's first content child must add another 16px of top spacing — mt-4 on a static container, or pt-4 when the element is the x-merge="append" target so the padding survives row appends — for a consistent 40px gap from tab strip to content.

Empty states inside a tab also need the 16px: <koala-empty-state class="mt-4" .../>. Card-wrapped tab content (e.g. activity timelines, settings forms) uses <div koala-card class="mt-4">; do not use mt-6, which produces a 48px gap and breaks parity with list-style tabs.

@* List-style tab — pt-4 on the x-merge target *@
<div id="user-quote-rows" x-merge="append" class="space-y-3 pt-4">
    @foreach (var quote in Model.Quotes) { ... }
</div>

@* Empty state — mt-4 on the tag helper *@
<koala-empty-state icon="Quote" title="No quotes" class="mt-4" />

@* Card-wrapped tab — mt-4 on the koala-card *@
<div koala-card class="mt-4">
    ...
</div>

Active tab click prevention

The currently active tab should prevent navigation by calling $event.preventDefault(). Inactive tabs trigger a skeleton and navigate via Alpine-AJAX.

<a href="/partner/quotes/view/abc?tab=details"
   x-target.push="main"
   x-on:click="@(Model.ActiveTab == "details"
       ? "$event.preventDefault()"
       : "showTabSkeleton('tab-container', 'form')")"
   class="@(Model.ActiveTab == "details"
       ? "text-primary dark:text-primary border-b-2 border-primary"
       : "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200")
       pb-3 text-sm font-medium">
    Details
</a>