Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Cards Laravel Package

harvirsidhu/filament-cards

Filament Cards turns any Filament page into a card-based navigation hub for settings hubs, cluster front pages, and resource dashboards. It auto-discovers authorized pages, supports grouping and layout, and feels native to Filament.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up Tailwind first — this is the step people skip.

composer require harvirsidhu/filament-cards

Then in your Filament theme.css:

@source '../../../../vendor/harvirsidhu/filament-cards/resources/views';
npm run build

Without that @source line the cards render unstyled (Tailwind never scans the vendor views). Requirements: PHP 8.2+, Laravel 11+, Filament v4 or v5.

Where to look first, in order:

  1. The README's "Which approach fits your case?" table — it maps your situation (Cluster / Resource / generic) to the correct discovery method in one glance.
  2. Harvirsidhu\FilamentCards\Filament\Pages\CardsPage — the base class you extend; getCards() is the only method you must implement.
  3. CardItem and CardGroup — the two primitives you'll spend 95% of your time with.
  4. discoverClusterCards() / discoverResourceCards() — the auto-discovery helpers, which are the real reason to use this over hand-rolling a Blade grid.

First use case — a 15-line standalone page:

class ControlPanel extends CardsPage
{
    protected static ?string $navigationIcon = 'heroicon-o-squares-2x2';

    protected static function getCards(): array
    {
        return [
            CardItem::make(CompanySettings::class),
            CardItem::make(BillingSettings::class),
        ];
    }
}

Filament auto-registers the page. CardItem::make() accepts a Page class, a Resource class, or a URL string — for classes it pulls label, icon, badge, badgeColor, and url straight off the navigation properties, so you write almost nothing. Ship this, then graduate to discoverClusterCards() once your Cluster has more than three pages.

Implementation Patterns

Cluster front page (the main event). Define the Cluster, then create a CardsPage inside it with $cluster set and $navigationSort = -1 so the hub sorts above the pages it links to:

class SettingsHub extends CardsPage
{
    protected static ?string $cluster = Settings::class;
    protected static ?int $navigationSort = -1;

    protected static function getCards(): array
    {
        return static::discoverClusterCards();
    }
}

Existing cluster pages need zero changes. Discovery walks every registered component, skips the CardsPage itself, calls canAccess(), checks visibility, then reads label/icon/URL/badge/description/group/sort. Breadcrumbs work automatically.

Instrument existing pages with static properties instead of editing the hub. This is the highest-leverage pattern — descriptions and grouping live on the page they describe:

class CompanySettings extends Page
{
    public static ?string $navigationDescription = 'Manage company name, address, and branding.';
    public static ?string $filamentCardsGroup = 'Business Settings';
}

New pages added to the Cluster appear on the hub with no hub edit. That's the property that keeps the hub from rotting.

Mix discovered and manual cards. Spread discovery into a normal array and append hand-built groups — external docs, status links, anything not a Filament page:

protected static function getCards(): array
{
    return [
        ...static::discoverClusterCards(),
        CardGroup::make('External Links')
            ->schema([
                CardItem::make('https://docs.example.com')
                    ->label('Documentation')
                    ->icon('heroicon-o-book-open')
                    ->openUrlInNewTab(),
            ]),
    ];
}

Order in the array is render order, so put the manual group last.

Resource hub. When a Resource has many custom pages, set $resource and use discoverResourceCards() — same hooks apply:

class UserSettingsHub extends CardsPage
{
    protected static string $resource = UserResource::class;

    protected static function getCards(): array
    {
        return static::discoverResourceCards();
    }
}

Authorization and progressive disclosure. Discovery honors canAccess(), so you get tenant- and policy-aware cards for free. For finer control use closures, evaluated at render time:

CardItem::make(BillingSettings::class)
    ->visible(fn () => auth()->user()->can('manage-billing'));

CardItem::make(DangerZone::class)
    ->disabled(fn () => ! auth()->user()->isAdmin());

Prefer disabled() over hidden() for admin-only areas: showing a greyed-out card communicates "this exists, ask for access", while hiding it makes the feature invisible and generates support tickets.

Layout control. Use columnSpan() with a responsive map rather than fixed integers, and let important cards breathe:

CardItem::make(CompanySettings::class)->columnSpan(['default' => 1, 'md' => 2, 'lg' => 3]);
CardItem::make(NotificationPrefs::class)->columnSpanFull();

Search, wired for real users. Enable $searchable = true on the page, then add aliases and jargon to the cards — they're filtered on but never displayed. This is what makes a 20-card hub usable:

CardItem::make(BillingSettings::class)
    ->searchKeywords(['invoices', 'payments', 'subscriptions', 'finance']);

Central exclude lists for the "internal" stuff. Cheaper than annotating every page:

protected static array $excludedClusterComponents = [AuditLogsResource::class, InternalToolsPage::class];
protected static array $excludedResourcePages = [UserResource\Pages\DangerZone::class];

Use this for cross-cutting lists; use showInFilamentCards() on the page itself when the page knows it shouldn't be advertised.

Dynamic registration (from a service provider) exists for modular/package apps where pages aren't known at build time — reach for it only when the static getCards() array can't express the set. Optional plugin registration is available but not required; a CardsPage subclass alone is enough.

Gotchas and Tips

  • The @source line is mandatory. If cards appear unstyled or with missing spacing/grid classes, 9 times out of 10 you skipped it or forgot npm run build after upgrading. Re-run the build after any package update.
  • showInFilamentCards falls back to shouldRegisterNavigation(). Counterintuitive both ways: a page hidden from the sidebar will also vanish from the hub unless you explicitly return true, and any page you don't want advertised needs an explicit false. Audit new clusters with this in mind.
  • Groups fall back to $navigationGroup. Your sidebar taxonomy leaks into the card grid by default. If the hub should be organized differently, set getFilamentCardsGroup() on the pages — don't fight it in getCards().
  • Don't forget $navigationSort = -1 on the hub. Discovery skips the CardsPage itself, but without a low sort it can land after the pages it's supposed to be the front door for.
  • Closures run per render. visible(), hidden(), disabled(), label(), and searchKeywords() all accept closures evaluated at render time — fine for auth()->user(), dangerous if you put a query in one and render 30 cards.
  • Badges can be an N+1. Discovery reads getNavigationBadge(), and that method often does a count(). On a hub with many resources, add ->badge(null) or override on the discovered cards if you notice slow page loads.
  • Search is client-side only. It filters what's already rendered — it cannot find cards that were hidden by canAccess(), visible(), or exclude lists. Set expectations with users accordingly, and lean on searchKeywords() for synonyms.
  • Manually-built CardItem::make(SomePage::class) deserves a ->visible() guard. Auto-discovery is documented to call canAccess(); for hand-written cards, don't assume the same and pair with an explicit policy check. It's one line and removes a class of accidental information leaks.
  • Typed static properties are required (protected static ?string $cluster, protected static string $resource) — PHP 8.2+ only, so check your CI matrix.
  • Early-adopter caution. 12 stars, 0 dependents, and a recent initial release cadence means you're on the leading edge. Pin the version in composer.json, read the CHANGELOG on every bump, and keep a regression check on your hub page. If you need heavy visual customization, publish/vendor the views rather than overriding from outside.
  • Debugging a missing card — walk the chain in order: is the component registered to the Cluster, does canAccess() return true, is showInFilamentCards / shouldRegisterNavigation true, is it in an exclude array, and does the CardsPage's own getCards() actually call discoverClusterCards()?
  • Extension points are deliberately thin and that's the right call: subclass CardsPage, override getCards(), use CardGroup for structure, use the exclude arrays for subtraction, and use dynamic registration for runtime addition. Prefer these over reaching into the package internals.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc