DEV3 Technologies home

Filament navigation badge showing a pending count

Show a cached count of items awaiting action as a coloured badge on a Filament 4 sidebar entry, with a tooltip and a hide-when-zero rule.

FilamentNavigationfilamentnavigationbadgecache

Code

<?php

namespace App\Filament\Resources\Disputes;

use App\Models\Dispute;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Support\Icons\Heroicon;
use Illuminate\Support\Facades\Cache;
use UnitEnum;

class DisputeResource extends Resource
{
    protected static ?string $model = Dispute::class;

    protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedShieldExclamation;

    protected static string|UnitEnum|null $navigationGroup = 'Support';

    protected static ?int $navigationSort = 20;

    private const BADGE_CACHE_KEY = 'nav.disputes.open';

    /** Count shared by the badge, colour and tooltip, cached for a minute. */
    protected static function openCount(): int
    {
        return Cache::remember(self::BADGE_CACHE_KEY, 60, fn () => Dispute::query()
            ->whereIn('status', ['open', 'awaiting_review'])
            ->count());
    }

    // Returning null hides the badge completely when nothing needs attention.
    public static function getNavigationBadge(): ?string
    {
        $count = static::openCount();

        return $count > 0 ? (string) min($count, 99).($count > 99 ? '+' : '') : null;
    }

    // Escalate the colour as the queue grows.
    public static function getNavigationBadgeColor(): string|array|null
    {
        return match (true) {
            static::openCount() >= 20 => 'danger',
            static::openCount() >= 5 => 'warning',
            default => 'success',
        };
    }

    public static function getNavigationBadgeTooltip(): ?string
    {
        return static::openCount().' disputes need a decision';
    }

    // Call from the Dispute model's saved/deleted events so the badge updates quickly.
    public static function forgetBadge(): void
    {
        Cache::forget(self::BADGE_CACHE_KEY);
    }
}

Needs: filament/filament@^4.0

How it works

What it solves

Moderators and operators should see at a glance where work is waiting. A sidebar badge with the number of pending items does that, but the navigation renders on every page load, so a naive count query runs on every click. This pattern keeps it cheap and informative.

How it works

openCount() is the only place that touches the database. It wraps a count of open and awaiting-review disputes in Cache::remember for 60 seconds, so all three badge hooks share one query and a busy panel runs it at most once a minute.

getNavigationBadge() returns a string, or null when the count is zero, which removes the badge entirely instead of showing a dull "0". Very large counts are capped as "99+". getNavigationBadgeColor() escalates from success to warning to danger as the queue grows, and getNavigationBadgeTooltip() explains the number on hover.

forgetBadge() is a small hook for freshness: call it from the model's saved and deleted events (or an observer) so the number drops right after a moderator resolves a case, rather than waiting for the TTL.

Adapting it

Change the statuses and thresholds, or return a different colour per role. For tenant apps include the tenant id in the cache key. If you do not want caching, remove Cache::remember and keep the query indexed on status.

Pitfalls

  • A badge computed per user (for example "assigned to me") needs the user id in the cache key.
  • Keep the query as a pure count; do not load models in navigation code.
  • Cache stores like array in tests will not share values between requests.

How to use it

  1. Merge the badge methods into an existing Filament resource (or copy the file and adjust the model).
  2. Index the status column of the table you count.
  3. Add an observer or model events that call DisputeResource::forgetBadge() when rows change.
  4. Reload the panel: the sidebar item shows the badge when there is pending work.
  5. Tune thresholds and colours to your own workload.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets