N

Counter sync

Badges and totals (unread count, cart size, active filters) stay fresh across every fragment response via HTMX out-of-band swaps.

#When to use

Any counter that's displayed in more than one place, or one place outside the region the user is mutating. Think: notifications bell, cart icon, "3 pending reviews" sidebar badge.

#Register the sync block

csharp
// Program.cs
builder.Services.AddHtmx(opts =>
{
    opts.RegisterSyncBlock("_NotificationsBadge");
});

#Layout

html
<partial name="_NotificationsBadge" />

#Partial

cshtml
@inject INotificationService Notifications
@{
    var unread = await Notifications.UnreadCountAsync(User.GetId());
}
<span id="_NotificationsBadge" hx-swap-oob="true" class="badge">@unread</span>

The hx-swap-oob="true" attribute tells htmx: "find the element with this id in the live DOM and replace it in place, regardless of where the request was targeted." That's how a single click on (e.g.) "mark as read" can update both the row that was clicked AND the badge in the header.

#Result

Every htmx response that flows through PageOrPartial appends _NotificationsBadge after the primary partial. Htmx sees hx-swap-oob="true", looks up the matching id in the page, and swaps it in place — no extra wiring per handler.

#Pure-Alpine alternative (no server roundtrip)

For counters that change purely from client interactions (e.g. live cart total as the user toggles items in a form), use an Alpine $store:

html
<div x-data x-init="$store.cart ??= { count: 0 }">
    <button x-on:click="$store.cart.count++">Add to cart</button>
</div>
@* Anywhere on the page: *@
<span x-text="$store.cart.count"></span>

#Keeping it cheap

Sync block partials run on every htmx response. Make the underlying query trivial — a cached count, a Redis GET, or a single-row SELECT. Don't put expensive aggregations in a sync block.