N

Tabs with lazy content

Each tab's body loads on first reveal, not when the page renders. Cuts initial HTML size and skips queries the user may never look at.

#When to use

Tab panels with heavy content — activity feeds, audit logs, analytics — where loading everything up front is wasteful.

#With + HTMX

Use the bundled component for the tab UI, and put a hx-trigger="revealed once" element inside each lazy panel:

cshtml
<noundry-tabs style="default">
    <noundry-tab-item title="Overview" active="true">
        @* Always loaded with the page. *@
        <partial name="_Overview" model="Model" />
    </noundry-tab-item>

    <noundry-tab-item title="Activity">
        <div hx-get="/projects/@Model.Id/activity"
             hx-trigger="revealed once"
             hx-swap="outerHTML">
            <div class="animate-pulse text-slate-400">Loading activity…</div>
        </div>
    </noundry-tab-item>

    <noundry-tab-item title="Audit log">
        <div hx-get="/projects/@Model.Id/audit"
             hx-trigger="revealed once"
             hx-swap="outerHTML">
            <div class="animate-pulse text-slate-400">Loading audit log…</div>
        </div>
    </noundry-tab-item>
</noundry-tabs>

hx-trigger="revealed once" fires when the element scrolls into view AND only the first time. Once the panel is loaded it stays in the DOM, so subsequent tab switches don't refetch.

#Handler

Expose the partial as its own minimal route:

csharp
public class ActivityModel : PageModel
{
    private readonly IActivityService _activity;
    public ActivityModel(IActivityService activity) => _activity = activity;

    public IReadOnlyList<ActivityEntry> Items { get; private set; } = [];

    public async Task<IActionResult> OnGetAsync(int id, CancellationToken ct)
    {
        Items = await _activity.RecentAsync(id, ct);
        return Partial("_ActivityList", Items);
    }
}

Or with Minimal API:

csharp
app.MapGet("/projects/{id:int}/activity", async (int id, IActivityService svc, CancellationToken ct) =>
{
    var items = await svc.RecentAsync(id, ct);
    return Results.Extensions.RazorComponent("_ActivityList", items);
});

#Why revealed not intersect

hx-trigger="intersect once" fires the moment any pixel of the element enters the viewport, even with display: none. revealed waits for it to actually become visible (CSS-displayed) — which is what you want when tabs flip via Alpine's x-show.