harvirsidhu/filament-cards

A Filament-native cards plugin for organizing pages and resources into a card-based navigation hub.
2,500 8
Install
composer require harvirsidhu/filament-cards
Latest Version:v1.0.6
PHP:^8.2
License:MIT
Last Updated:Feb 19, 2026
Links: GitHub  ·  Packagist
Maintainer: harvirsidhu

Filament Cards

Turn any Filament page into a card-based navigation hub — perfect for Settings hubs, Cluster front pages, Resource dashboards, or any place you want a clean grid of links instead of a sidebar tree.

It feels like part of Filament: same API patterns (label, schema, columnSpan, visible/hidden), respects your existing navigation config, and auto-discovers Cluster/Resource pages with full authorization checks.

Filament Cards screenshot


Table of Contents

  1. Why use this?
  2. Which approach fits your case?
  3. Requirements
  4. Installation
  5. 60-Second Quick Start
  6. Use Case A — Cluster Front Page (most common)
  7. Use Case B — Resource Hub
  8. Use Case C — Standalone Settings Page
  9. API Reference — CardItem
  10. API Reference — CardGroup
  11. API Reference — CardsPage Configuration
  12. API Reference — Page/Resource Hooks (for auto-discovery)
  13. Advanced — Dynamic Registration
  14. Advanced — Custom Discovery Filtering
  15. Full Example
  16. Optional — Plugin Registration
  17. License

Why use this?

Feature What it gives you
Auto-discovery Reads pages/resources in a Cluster (or Resource) and creates cards automatically — no manual list to maintain.
Filament-native API label, description, schema, visible, hidden, columnSpan — same patterns you already use.
Authorization-aware Calls canAccess() on each component before rendering.
Grouping & layout CardGroup, columns, spans, compact mode, collapsible sections.
Flexible visibility Per-card or per-page hooks; central exclude lists; closure-based conditions.
Manual + discovered Mix auto-discovered cards with hand-crafted ones (e.g., external links).
Dynamic registration Add cards from service providers — useful for modular apps and packages.
Client-side search Optional live search that filters by label, description, badge, and custom keywords.

Which approach fits your case?

Your situation Use this
You have a Cluster with several pages and want a landing page Cluster Front Page
You have a Resource with many custom pages Resource Hub
You just want a generic settings/control panel with manual cards Standalone Page
You want to add cards from a package or module Dynamic Registration

Requirements

  • PHP 8.2+
  • Laravel 11+
  • Filament v4 or v5

Installation

Install the package via Composer:

composer require harvirsidhu/filament-cards

Theme setup (required for styling)

The plugin uses Tailwind classes, so add its views to your Filament theme so Tailwind picks them up.

In your theme.css, add:

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

Then rebuild assets:

npm run build

60-Second Quick Start

The smallest possible cards page:

namespace App\Filament\Pages;

use Harvirsidhu\FilamentCards\CardItem;
use Harvirsidhu\FilamentCards\Filament\Pages\CardsPage;

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),
        ];
    }
}

That's it — Filament will pick up your new page, and clicking each card navigates to the linked page.


Use Case A — Cluster Front Page (most common)

The most powerful pattern: a CardsPage that automatically lists every page and resource in its Cluster.

Step 1 — Define the Cluster

namespace App\Filament\Clusters;

use Filament\Clusters\Cluster;

class Settings extends Cluster
{
    protected static ?string $navigationIcon = 'heroicon-o-cog-8-tooth';
}

Step 2 — Create the front-page CardsPage

namespace App\Filament\Clusters\Settings\Pages;

use App\Filament\Clusters\Settings;
use Harvirsidhu\FilamentCards\Filament\Pages\CardsPage;

class SettingsHub extends CardsPage
{
    protected static ?string $cluster = Settings::class;
    protected static ?int $navigationSort = -1; // Show first in the cluster

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

Step 3 — Your other Cluster pages need no changes

Your existing pages just work. To add a description on the card, add a single property:

class CompanySettings extends Page
{
    protected static ?string $cluster = Settings::class;
    protected static ?string $navigationIcon = 'heroicon-o-building-office';

    public static ?string $navigationDescription = 'Manage company name, address, and branding.';
}

Breadcrumbs (Dashboard > Settings > Company Settings) work automatically.

What discoverClusterCards() does

In order, for every component registered to the Cluster:

  1. Skips the CardsPage itself.
  2. Calls canAccess() (respects authorization).
  3. Checks showInFilamentCards() / $showInFilamentCards — falls back to shouldRegisterNavigation().
  4. Reads $navigationLabel, $navigationIcon, and the resolved URL.
  5. Reads getNavigationBadge() / getNavigationBadgeColor() if defined.
  6. Reads $navigationDescription (or getNavigationDescription()) if defined.
  7. Groups by getFilamentCardsGroup() / $filamentCardsGroup — falls back to $navigationGroup.
  8. Sorts each group by $navigationSort.

Hiding a card from auto-discovery

Option 1 — on the Page/Resource (recommended):

class InternalToolsPage extends Page
{
    public static function showInFilamentCards(): bool
    {
        return false;
    }
}

// or as a property
class AuditLogsResource extends Resource
{
    public static bool $showInFilamentCards = false;
}

To force-show a page that's hidden from the sidebar:

class HiddenFromSidebarPage extends Page
{
    public static function shouldRegisterNavigation(): bool
    {
        return false;
    }

    public static function showInFilamentCards(): bool
    {
        return true;
    }
}

Option 2 — central exclude list on the CardsPage:

class SettingsHub extends CardsPage
{
    protected static array $excludedClusterComponents = [
        AuditLogsResource::class,
        InternalToolsPage::class,
    ];
}

Custom group name (different from the sidebar)

class CompanySettings extends Page
{
    public static function getFilamentCardsGroup(): ?string
    {
        return 'Business Settings';
    }
}

// or property form
class BillingResource extends Resource
{
    public static ?string $filamentCardsGroup = 'Finance';
}

Mixing discovered cards with manual ones

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(),
            ]),
    ];
}

Use Case B — Resource Hub

When a Resource has many custom pages, use discoverResourceCards() to auto-create a card for each:

namespace App\Filament\Resources\UserResource\Pages;

use App\Filament\Resources\UserResource;
use Harvirsidhu\FilamentCards\Filament\Pages\CardsPage;

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

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

Same hooks (showInFilamentCards, $navigationDescription, etc.) work here. To exclude specific pages:

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

Use Case C — Standalone Settings Page

No Cluster, no Resource — just a manually-curated cards page:

use Harvirsidhu\FilamentCards\CardGroup;
use Harvirsidhu\FilamentCards\CardItem;
use Harvirsidhu\FilamentCards\Filament\Pages\CardsPage;

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

    protected static function getCards(): array
    {
        return [
            CardGroup::make('General')
                ->icon('heroicon-o-cog')
                ->description('Core settings')
                ->schema([
                    CardItem::make(CompanySettings::class)->color('primary'),
                    CardItem::make(BillingSettings::class)->color('success'),
                ]),

            CardItem::make('/external/docs')
                ->label('Documentation')
                ->icon('heroicon-o-document-text')
                ->openUrlInNewTab(),
        ];
    }
}

API Reference — CardItem

CardItem represents a single clickable card. Pass a Filament Page class, Resource class, or a URL string:

CardItem::make(CompanySettings::class)  // Filament Page
CardItem::make(UserResource::class)     // Filament Resource
CardItem::make('/custom/path')          // Internal URL
CardItem::make('https://example.com')   // External URL

When given a Page/Resource class, the card auto-resolves label, icon, badge, badgeColor, and url from the class's navigation properties.

Method overview

Method Purpose
label() Override the card title
description() Subtitle below the title
badge() Right-aligned badge
badgeColor() Badge color
icon() Override the card icon
url() / openUrlInNewTab() Override target URL
alignment() Text alignment inside the card
color() Color accent on the card
visible() / hidden() Conditional rendering
disabled() Render but make non-clickable
sort() Order within a group
columnSpan() / columnSpanFull() Grid span
searchKeywords() Extra terms for the search bar
extraAttributes() Custom HTML attributes

label()

Override the card title. Accepts a string or Closure:

CardItem::make(CompanySettings::class)
    ->label('Company')

CardItem::make(CompanySettings::class)
    ->label(fn () => __('settings.company'))

description()

Add a subtitle below the title:

CardItem::make(CompanySettings::class)
    ->description('Manage company name, address, and branding')

Auto-discovery shortcut: add public static ?string $navigationDescription to the page/resource and discoverClusterCards() / discoverResourceCards() will pick it up automatically.

badge() / badgeColor()

CardItem::make(CompanySettings::class)
    ->badge('Beta')
    ->badgeColor('primary')

Auto-discovery: read from getNavigationBadge() / getNavigationBadgeColor() when present on the discovered Page/Resource.

icon()

CardItem::make('/path')
    ->icon('heroicon-o-building-office')

url() / openUrlInNewTab()

CardItem::make(CompanySettings::class)
    ->url('https://custom-url.com')
    ->openUrlInNewTab()

alignment()

Text alignment inside the card. Options: Start, Center, End, Justify.

use Filament\Support\Enums\Alignment;

CardItem::make(CompanySettings::class)
    ->alignment(Alignment::Center)

color()

Color accent on the card. Available: primary, success, danger, warning, info, gray.

CardItem::make(BillingSettings::class)->color('success')
CardItem::make(DangerZone::class)->color('danger')

visible() / hidden()

Boolean or Closure:

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

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

disabled()

Render the card but make it non-clickable with reduced opacity:

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

sort()

Order cards within a group:

CardItem::make(CompanySettings::class)->sort(1)
CardItem::make(BillingSettings::class)->sort(2)

columnSpan() / columnSpanFull()

CardItem::make(CompanySettings::class)->columnSpan(2)
CardItem::make(NotificationPrefs::class)->columnSpanFull()

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

searchKeywords()

Extra terms used by the page's search bar (when $searchable = true on the CardsPage). Useful for aliases and jargon — the keywords are not displayed.

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

CardItem::make(CompanySettings::class)
    ->searchKeywords('organisation') // single string ok

CardItem::make(LegacyTools::class)
    ->searchKeywords(fn () => $this->resolveLegacyAliases())

Auto-discovery: declare on the page/resource directly:

public static array $filamentCardsSearchKeywords = ['organisation', 'org'];

// …or as a method
public static function getFilamentCardsSearchKeywords(): array
{
    return ['organisation', 'org'];
}

extraAttributes()

Custom HTML attributes on the card element:

CardItem::make(CompanySettings::class)
    ->extraAttributes([
        'data-analytics' => 'company-settings',
        'id' => 'company-card',
    ])

API Reference — CardGroup

Groups organize cards under a (collapsible) header — like Filament's Section.

Method overview

Method Purpose
schema() Cards inside the group
label() / description() / icon() Header customization
columns() Override grid columns for this group
collapsible() / collapsed() Collapse behavior
compact() Tighter padding/gaps
visible() / hidden() Hide the whole group

schema()

CardGroup::make('General')
    ->schema([
        CardItem::make(CompanySettings::class),
        CardItem::make(BillingSettings::class),
    ])

label() / description() / icon() (group)

CardGroup::make('General')
    ->label('General Settings')
    ->description('Core application configuration')
    ->icon('heroicon-o-cog')
    ->schema([...])

columns()

Integer or responsive array (Filament widget style):

CardGroup::make('Wide Cards')
    ->columns(2)
    ->schema([...])

CardGroup::make('Wide Cards')
    ->columns([
        'md' => 2,
        'xl' => 4,
    ])
    ->schema([...])

collapsible() / collapsed()

CardGroup::make('Advanced')->collapsible()->schema([...])

// Starts collapsed (implicitly collapsible)
CardGroup::make('Advanced')->collapsed()->schema([...])

CardGroup::make('Advanced')
    ->collapsed(fn () => ! auth()->user()->isAdmin())
    ->schema([...])

compact()

CardGroup::make('Quick Links')
    ->compact()
    ->schema([...])

Group-level visible() / hidden()

CardGroup::make('Admin Only')
    ->hidden(fn () => ! auth()->user()->isAdmin())
    ->schema([...])

API Reference — CardsPage Configuration

Configure the whole page with static properties.

Property Type Default Purpose
$columns int|string|array 3 Grid columns (responsive supported)
$itemsAlignment Alignment Center Alignment of card content
$iconSize IconSize Medium Card icon size
$iconInlined bool false Inline icon with title (vs stacked)
$iconPosition IconPosition Before Icon before or after the label
$searchable bool false Show a client-side search bar
$searchPlaceholder ?string null ('Search…') Placeholder text for the search input
$excludedClusterComponents array [] Skip these classes in discoverClusterCards()
$excludedResourcePages array [] Skip these classes in discoverResourceCards()

$columns

protected static string|int|array $columns = 4;

// Responsive
protected static string|int|array $columns = [
    'md' => 2,
    'xl' => 4,
];

$itemsAlignment

use Filament\Support\Enums\Alignment;

protected static Alignment $itemsAlignment = Alignment::Center;

$iconSize

use Filament\Support\Enums\IconSize;

protected static IconSize $iconSize = IconSize::Small;

$iconInlined

protected static bool $iconInlined = true;

$iconPosition

use Filament\Support\Enums\IconPosition;

protected static IconPosition $iconPosition = IconPosition::After;

$searchable

Renders a search bar at the top right. Filters cards live by label, description, badge, and any searchKeywords(). Empty groups are auto-hidden. Filtering happens client-side via Alpine.js — no server round-trips.

protected static bool $searchable = true;

$searchPlaceholder

protected static bool $searchable = true;
protected static ?string $searchPlaceholder = 'Find a tool...';

$excludedClusterComponents

protected static array $excludedClusterComponents = [
    AuditLogsResource::class,
    InternalToolsPage::class,
];

$excludedResourcePages

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

API Reference — Page/Resource Hooks (for auto-discovery)

These are read off your existing Pages and Resources by discoverClusterCards() / discoverResourceCards(). You add them only to pages you want discovered.

Hook Form Purpose
$navigationDescription public static ?string Subtitle text on the card
getNavigationDescription() public static function (): ?string Same as above (method form)
$showInFilamentCards public static bool Whether to include in cards
showInFilamentCards() public static function (): bool Same (method form)
$filamentCardsGroup public static ?string Override group name (otherwise $navigationGroup)
getFilamentCardsGroup() public static function (): ?string Same (method form)
$filamentCardsSearchKeywords public static array Extra search terms
getFilamentCardsSearchKeywords() public static function (): array Same (method form)
getNavigationBadge() Filament built-in Read by discovery for the card badge
getNavigationBadgeColor() Filament built-in Read by discovery for the badge color
$navigationLabel / $navigationIcon / $navigationSort / $navigationGroup Filament built-in Used as defaults for label/icon/order/group

Advanced — Dynamic Registration

Add cards from outside the class — useful for modular apps and packages. Call from a service provider's boot():

use App\Filament\Pages\ControlPanel;
use Harvirsidhu\FilamentCards\CardItem;

ControlPanel::addCards([
    CardItem::make(UserManagement::class)
        ->label('User Accounts')
        ->icon('heroicon-o-users')
        ->description('Manage roles, permissions, and user accounts'),
]);

Advanced — Custom Discovery Filtering

Override shouldIncludeDiscoveredCard() for centralized inclusion logic that goes beyond exclude lists:

protected static function shouldIncludeDiscoveredCard(string $component): bool
{
    if (! parent::shouldIncludeDiscoveredCard($component)) {
        return false;
    }

    return $component !== BetaFeaturePage::class;
}

Full Example

Everything together — auto-discovery, manual cards, groups, conditional visibility, and an external link:

use Filament\Support\Enums\Alignment;
use Filament\Support\Enums\IconPosition;
use Filament\Support\Enums\IconSize;
use Harvirsidhu\FilamentCards\CardGroup;
use Harvirsidhu\FilamentCards\CardItem;
use Harvirsidhu\FilamentCards\Filament\Pages\CardsPage;

class SettingsHub extends CardsPage
{
    protected static ?string $navigationIcon = 'heroicon-o-cog-8-tooth';
    protected static int $columns = 3;
    protected static Alignment $itemsAlignment = Alignment::Center;
    protected static IconSize $iconSize = IconSize::Medium;
    protected static IconPosition $iconPosition = IconPosition::Before;

    protected static function getCards(): array
    {
        return [
            CardGroup::make('General')
                ->icon('heroicon-o-cog')
                ->description('Core application settings')
                ->collapsible()
                ->schema([
                    CardItem::make(CompanySettings::class)
                        ->color('primary'),

                    CardItem::make(BillingSettings::class)
                        ->visible(fn () => auth()->user()->can('manage-billing'))
                        ->color('success')
                        ->sort(2),

                    CardItem::make(NotificationPrefs::class)
                        ->description('Email, SMS & push notification preferences')
                        ->columnSpanFull(),
                ]),

            CardGroup::make('Danger Zone')
                ->icon('heroicon-o-exclamation-triangle')
                ->collapsed()
                ->columns(2)
                ->schema([
                    CardItem::make(DangerZone::class)
                        ->color('danger')
                        ->disabled(fn () => ! auth()->user()->isAdmin()),
                ]),

            CardItem::make('https://docs.example.com')
                ->label('Documentation')
                ->icon('heroicon-o-book-open')
                ->openUrlInNewTab()
                ->extraAttributes(['data-track' => 'docs']),
        ];
    }
}

Optional — Plugin Registration

Not required, but you can register the plugin in your panel provider for clarity:

use Harvirsidhu\FilamentCards\FilamentCardsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            FilamentCardsPlugin::make(),
        ]);
}

License

MIT — see License File.