A Filament-native cards plugin for organizing pages and resources into a card-based navigation hub.
harvirsidhu/filament-cards is a Laravel package for a filament-native cards plugin for organizing pages and resources into a card-based navigation hub..
It currently has 12 GitHub stars and 9.492 downloads on Packagist (latest version v1.0.9).
Install it with composer require harvirsidhu/filament-cards.
Discover more Laravel packages by harvirsidhu
or browse all Laravel packages to compare alternatives.
Last updated
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.

CardItemCardGroupCardsPage Configuration| 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. |
| 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 |
Install the package via Composer:
composer require harvirsidhu/filament-cards
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
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.
The most powerful pattern: a CardsPage that automatically lists every page and resource in its Cluster.
namespace App\Filament\Clusters;
use Filament\Clusters\Cluster;
class Settings extends Cluster
{
protected static ?string $navigationIcon = 'heroicon-o-cog-8-tooth';
}
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();
}
}
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.
discoverClusterCards() doesIn order, for every component registered to the Cluster:
canAccess() (respects authorization).showInFilamentCards() / $showInFilamentCards — falls back to shouldRegisterNavigation().$navigationLabel, $navigationIcon, and the resolved URL.getNavigationBadge() / getNavigationBadgeColor() if defined.$navigationDescription (or getNavigationDescription()) if defined.getFilamentCardsGroup() / $filamentCardsGroup — falls back to $navigationGroup.$navigationSort.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,
];
}
class CompanySettings extends Page
{
public static function getFilamentCardsGroup(): ?string
{
return 'Business Settings';
}
}
// or property form
class BillingResource extends Resource
{
public static ?string $filamentCardsGroup = 'Finance';
}
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(),
]),
];
}
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,
];
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(),
];
}
}
CardItemCardItem 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 | 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 $navigationDescriptionto the page/resource anddiscoverClusterCards()/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',
])
CardGroupGroups organize cards under a (collapsible) header — like Filament's Section.
| 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([...])
visible() / hidden()CardGroup::make('Admin Only')
->hidden(fn () => ! auth()->user()->isAdmin())
->schema([...])
CardsPage ConfigurationConfigure 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() |
$columnsprotected static string|int|array $columns = 4;
// Responsive
protected static string|int|array $columns = [
'md' => 2,
'xl' => 4,
];
$itemsAlignmentuse Filament\Support\Enums\Alignment;
protected static Alignment $itemsAlignment = Alignment::Center;
$iconSizeuse Filament\Support\Enums\IconSize;
protected static IconSize $iconSize = IconSize::Small;
$iconInlinedprotected static bool $iconInlined = true;
$iconPositionuse Filament\Support\Enums\IconPosition;
protected static IconPosition $iconPosition = IconPosition::After;
$searchableRenders 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;
$searchPlaceholderprotected static bool $searchable = true;
protected static ?string $searchPlaceholder = 'Find a tool...';
$excludedClusterComponentsprotected static array $excludedClusterComponents = [
AuditLogsResource::class,
InternalToolsPage::class,
];
$excludedResourcePagesprotected static array $excludedResourcePages = [
UserResource\Pages\DangerZone::class,
];
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 |
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'),
]);
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;
}
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']),
];
}
}
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(),
]);
}
MIT — see License File.