Skip to content

Admin Components

Reusable UI components for building admin panels: cards, tables, stat widgets, and more.

Cards

Basic Card

blade
<x-admin::card>
    <x-slot:header>
        <h3>Recent Posts</h3>
    </x-slot:header>

    <p>Card content goes here...</p>

    <x-slot:footer>
        <a href="{{ route('posts.index') }}">View All</a>
    </x-slot:footer>
</x-admin::card>

Card with Actions

blade
<x-admin::card>
    <x-slot:header>
        <h3>Post Statistics</h3>
        <x-slot:actions>
            <x-admin::button size="sm" wire:click="refresh">
                Refresh
            </x-admin::button>
        </x-slot:actions>
    </x-slot:header>

    <div class="stats">
        {{-- Statistics content --}}
    </div>
</x-admin::card>

Card Grid

Display cards in responsive grid:

blade
<x-admin::card-grid>
    <x-admin::card>
        <h4>Total Posts</h4>
        <p class="text-3xl">1,234</p>
    </x-admin::card>

    <x-admin::card>
        <h4>Published</h4>
        <p class="text-3xl">856</p>
    </x-admin::card>

    <x-admin::card>
        <h4>Drafts</h4>
        <p class="text-3xl">378</p>
    </x-admin::card>
</x-admin::card-grid>

Stat Widgets

Simple Stat

blade
<x-admin::stat
    label="Total Revenue"
    value="£45,231"
    icon="heroicon-o-currency-pound"
    color="green"
/>

Stat with Trend

blade
<x-admin::stat
    label="Active Users"
    :value="$activeUsers"
    icon="heroicon-o-users"
    :trend="$userTrend"
    trendLabel="vs last month"
/>

Trend Indicators:

  • Positive number: green up arrow
  • Negative number: red down arrow
  • Zero: neutral indicator

Stat with Chart

blade
<x-admin::stat
    label="Page Views"
    :value="$pageViews"
    icon="heroicon-o-eye"
    :sparkline="$viewsData"
/>

Sparkline Data:

php
public function getSparklineData()
{
    return [
        120, 145, 132, 158, 170, 165, 180, 195, 185, 200
    ];
}

Stat Grid

blade
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
    <x-admin::stat
        label="Total Posts"
        :value="$stats['total']"
        icon="heroicon-o-document-text"
    />

    <x-admin::stat
        label="Published"
        :value="$stats['published']"
        icon="heroicon-o-check-circle"
        color="green"
    />

    <x-admin::stat
        label="Drafts"
        :value="$stats['drafts']"
        icon="heroicon-o-pencil"
        color="yellow"
    />

    <x-admin::stat
        label="Archived"
        :value="$stats['archived']"
        icon="heroicon-o-archive-box"
        color="gray"
    />
</div>

Tables

Basic Table

blade
<x-admin::table>
    <x-slot:header>
        <x-admin::table.th>Title</x-admin::table.th>
        <x-admin::table.th>Author</x-admin::table.th>
        <x-admin::table.th>Status</x-admin::table.th>
        <x-admin::table.th>Actions</x-admin::table.th>
    </x-slot:header>

    @foreach($posts as $post)
        <x-admin::table.tr>
            <x-admin::table.td>{{ $post->title }}</x-admin::table.td>
            <x-admin::table.td>{{ $post->author->name }}</x-admin::table.td>
            <x-admin::table.td>
                <x-admin::badge :color="$post->status_color">
                    {{ $post->status }}
                </x-admin::badge>
            </x-admin::table.td>
            <x-admin::table.td>
                <x-admin::button size="sm" wire:click="edit({{ $post->id }})">
                    Edit
                </x-admin::button>
            </x-admin::table.td>
        </x-admin::table.tr>
    @endforeach
</x-admin::table>

Sortable Table

blade
<x-admin::table>
    <x-slot:header>
        <x-admin::table.th sortable wire:click="sortBy('title')" :active="$sortField === 'title'">
            Title
        </x-admin::table.th>
        <x-admin::table.th sortable wire:click="sortBy('created_at')" :active="$sortField === 'created_at'">
            Created
        </x-admin::table.th>
    </x-slot:header>

    {{-- Table rows --}}
</x-admin::table>

Livewire Component:

php
class PostsTable extends Component
{
    public $sortField = 'created_at';
    public $sortDirection = 'desc';

    public function sortBy($field)
    {
        if ($this->sortField === $field) {
            $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortField = $field;
            $this->sortDirection = 'asc';
        }
    }

    public function render()
    {
        $posts = Post::orderBy($this->sortField, $this->sortDirection)
            ->paginate(20);

        return view('livewire.posts-table', compact('posts'));
    }
}

Table with Bulk Actions

blade
<x-admin::table>
    <x-slot:header>
        <x-admin::table.th>
            <x-admin::checkbox wire:model.live="selectAll" />
        </x-admin::table.th>
        <x-admin::table.th>Title</x-admin::table.th>
        <x-admin::table.th>Actions</x-admin::table.th>
    </x-slot:header>

    @foreach($posts as $post)
        <x-admin::table.tr>
            <x-admin::table.td>
                <x-admin::checkbox wire:model.live="selected" value="{{ $post->id }}" />
            </x-admin::table.td>
            <x-admin::table.td>{{ $post->title }}</x-admin::table.td>
            <x-admin::table.td>...</x-admin::table.td>
        </x-admin::table.tr>
    @endforeach
</x-admin::table>

@if(count($selected) > 0)
    <div class="bulk-actions">
        <p>{{ count($selected) }} selected</p>
        <x-admin::button wire:click="bulkPublish">Publish</x-admin::button>
        <x-admin::button wire:click="bulkDelete" color="red">Delete</x-admin::button>
    </div>
@endif

Badges

Status Badges

blade
<x-admin::badge color="green">Published</x-admin::badge>
<x-admin::badge color="yellow">Draft</x-admin::badge>
<x-admin::badge color="red">Archived</x-admin::badge>
<x-admin::badge color="blue">Scheduled</x-admin::badge>
<x-admin::badge color="gray">Pending</x-admin::badge>

Badge with Dot

blade
<x-admin::badge color="green" dot>
    Active
</x-admin::badge>

Badge with Icon

blade
<x-admin::badge color="blue">
    <x-slot:icon>
        <svg>...</svg>
    </x-slot:icon>
    Verified
</x-admin::badge>

Removable Badge

blade
<x-admin::badge
    color="blue"
    removable
    wire:click="removeTag({{ $tag->id }})"
>
    {{ $tag->name }}
</x-admin::badge>

Alerts

Basic Alert

blade
<x-admin::alert type="success">
    Post published successfully!
</x-admin::alert>

<x-admin::alert type="error">
    Failed to save post. Please try again.
</x-admin::alert>

<x-admin::alert type="warning">
    This post has not been reviewed yet.
</x-admin::alert>

<x-admin::alert type="info">
    You have 3 draft posts.
</x-admin::alert>

Dismissible Alert

blade
<x-admin::alert type="success" dismissible>
    Post published successfully!
</x-admin::alert>

Alert with Title

blade
<x-admin::alert type="warning">
    <x-slot:title>
        Pending Review
    </x-slot:title>
    This post requires approval before it can be published.
</x-admin::alert>

Empty States

Basic Empty State

blade
<x-admin::empty-state>
    <x-slot:icon>
        <svg>...</svg>
    </x-slot:icon>

    <x-slot:title>
        No posts yet
    </x-slot:title>

    <x-slot:description>
        Get started by creating your first blog post.
    </x-slot:description>

    <x-slot:action>
        <x-admin::button wire:click="create">
            Create Post
        </x-admin::button>
    </x-slot:action>
</x-admin::empty-state>

Search Empty State

blade
@if($posts->isEmpty() && $search)
    <x-admin::empty-state>
        <x-slot:title>
            No results found
        </x-slot:title>

        <x-slot:description>
            No posts match your search for "{{ $search }}".
        </x-slot:description>

        <x-slot:action>
            <x-admin::button wire:click="clearSearch">
                Clear Search
            </x-admin::button>
        </x-slot:action>
    </x-admin::empty-state>
@endif

Loading States

Skeleton Loaders

blade
<x-admin::skeleton type="card" />
<x-admin::skeleton type="table" rows="5" />
<x-admin::skeleton type="text" lines="3" />

Loading Spinner

blade
<div wire:loading>
    <x-admin::spinner />
</div>

<div wire:loading.remove>
    {{-- Content --}}
</div>

Loading Overlay

blade
<div wire:loading.class="opacity-50 pointer-events-none">
    {{-- Content becomes translucent while loading --}}
</div>

<div wire:loading class="loading-overlay">
    <x-admin::spinner size="lg" />
</div>

Pagination

blade
<x-admin::table>
    {{-- Table content --}}
</x-admin::table>

{{ $posts->links('admin::pagination') }}

Custom Pagination:

blade
<nav class="pagination">
    {{ $posts->appends(request()->query())->links() }}
</nav>

Modals (See Modals Documentation)

See Livewire Modals → for full modal documentation.

Basic Dropdown

blade
<x-admin::dropdown>
    <x-slot:trigger>
        <x-admin::button>
            Actions
        </x-admin::button>
    </x-slot:trigger>

    <x-admin::dropdown.item wire:click="edit">
        Edit
    </x-admin::dropdown.item>

    <x-admin::dropdown.item wire:click="duplicate">
        Duplicate
    </x-admin::dropdown.item>

    <x-admin::dropdown.divider />

    <x-admin::dropdown.item wire:click="delete" color="red">
        Delete
    </x-admin::dropdown.item>
</x-admin::dropdown>
blade
<x-admin::dropdown>
    <x-slot:trigger>
        <button>⋮</button>
    </x-slot:trigger>

    <x-admin::dropdown.item wire:click="edit">
        <x-slot:icon>
            <svg>...</svg>
        </x-slot:icon>
        Edit Post
    </x-admin::dropdown.item>

    <x-admin::dropdown.item wire:click="view">
        <x-slot:icon>
            <svg>...</svg>
        </x-slot:icon>
        View
    </x-admin::dropdown.item>
</x-admin::dropdown>

Tabs

blade
<x-admin::tabs>
    <x-admin::tab
        name="general"
        label="General"
        :active="$activeTab === 'general'"
        wire:click="$set('activeTab', 'general')"
    >
        {{-- General settings --}}
    </x-admin::tab>

    <x-admin::tab
        name="seo"
        label="SEO"
        :active="$activeTab === 'seo'"
        wire:click="$set('activeTab', 'seo')"
    >
        {{-- SEO settings --}}
    </x-admin::tab>

    <x-admin::tab
        name="advanced"
        label="Advanced"
        :active="$activeTab === 'advanced'"
        wire:click="$set('activeTab', 'advanced')"
    >
        {{-- Advanced settings --}}
    </x-admin::tab>
</x-admin::tabs>

Best Practices

1. Use Semantic Components

blade
{{-- ✅ Good - semantic component --}}
<x-admin::stat
    label="Revenue"
    :value="$revenue"
    icon="heroicon-o-currency-pound"
/>

{{-- ❌ Bad - manual markup --}}
<div class="stat">
    <p>Revenue</p>
    <span>{{ $revenue }}</span>
</div>

2. Consistent Colors

blade
{{-- ✅ Good - use color props --}}
<x-admin::badge color="green">Active</x-admin::badge>
<x-admin::badge color="red">Inactive</x-admin::badge>

{{-- ❌ Bad - custom classes --}}
<span class="bg-green-500">Active</span>

3. Loading States

blade
{{-- ✅ Good - show loading state --}}
<div wire:loading>
    <x-admin::spinner />
</div>

{{-- ❌ Bad - no feedback --}}
<button wire:click="save">Save</button>

4. Empty States

blade
{{-- ✅ Good - helpful empty state --}}
@if($posts->isEmpty())
    <x-admin::empty-state>
        <x-slot:action>
            <x-admin::button wire:click="create">
                Create First Post
            </x-admin::button>
        </x-slot:action>
    </x-admin::empty-state>
@endif

{{-- ❌ Bad - no guidance --}}
@if($posts->isEmpty())
    <p>No posts</p>
@endif

Testing Components

php
use Tests\TestCase;

class ComponentsTest extends TestCase
{
    public function test_stat_widget_renders(): void
    {
        $view = $this->blade('<x-admin::stat label="Users" value="100" />');

        $view->assertSee('Users');
        $view->assertSee('100');
    }

    public function test_badge_renders_with_color(): void
    {
        $view = $this->blade('<x-admin::badge color="green">Active</x-admin::badge>');

        $view->assertSee('Active');
        $view->assertSeeInOrder(['class', 'green']);
    }
}

Learn More

Released under the EUPL-1.2 License.