Skip to content

Badges

A badge next to an entry shows a count or a short status, e.g. the number of open tickets.

final class TicketResource extends Resource
{
public function navigationBadge(): ?string
{
$open = $this->query()->where('status', 'open')->countAllResults();
return $open > 0 ? (string) $open : null;
}
public function navigationBadgeColor(): string
{
return 'danger';
}
}
Method Default Purpose
navigationBadge(): ?string null text of the badge; null shows none
navigationBadgeColor(): string secondary primary, secondary, success, danger, warning, info, light, dark

Pages have the same two methods. The badge is escaped and also part of the tooltip when the sidebar is reduced to icons.

$this->query() of a resource is scoped like its list: the current tenant and scopeQuery() (e.g. “only my records”). A badge therefore never counts records the user cannot see in the list.

// Tasks of the current user on a page
public function navigationBadge(): ?string
{
$count = model(TaskModel::class)->where('assignee_id', auth()->id())->where('done_at', null)->countAllResults();
return $count === 0 ? null : ($count > 99 ? '99+' : (string) $count);
}

The badge is computed on every page view of the panel, for each entry the user may open. Keep it cheap:

  • count on an indexed column (status, assignee_id),
  • no joins over large tables,
  • or cache the value for a short time per tenant:
public function navigationBadge(): ?string
{
$key = 'badge.tickets.' . service('tenantContext')->id();
$open = cache()->remember($key, 60, fn (): int => $this->query()->where('status', 'open')->countAllResults());
return $open > 0 ? (string) $open : null;
}

A cached badge can be up to a minute old; the list itself is always current. Badges are not refreshed while a page is open.

The inbox shows the number of open tasks of the user. The notification bell in the topbar has its own count, refreshed every minute, see Notifications.