mrshanebarron/empty-state
mrshanebarron/empty-state is a Laravel package for showing polished, consistent empty states across your app. It helps you render friendly messages, icons, and call-to-action buttons whenever lists, tables, or search results return no data, with simple Blade integration and customizable defaults.
Install and inspect before you build.
composer require mrshanebarron/empty-state
php artisan vendor:publish --provider="MrShaneBarron\EmptyState\EmptyStateServiceProvider"
The publish step exposes views (and usually a config file). Skip publishing on day one — use the package defaults first so you can see what you're overriding later.
Where to look first, in order:
vendor/mrshanebarron/empty-state/src/View/Components/ — the class backing the component. This tells you the exact prop names, defaults, and whether the component is anonymous or class-based. Note the v1.0.2 fix here: hyphenated names now resolve to PascalCase classes, so a component registered as empty-state maps to EmptyState.vendor/mrshanebarron/empty-state/resources/views/ — the markup you'll actually be looking at every day, and the markup Tailwind needs to scan.--tag=empty-state-views, --tag=empty-state-config) and whether a @emptyState directive is registered.config/empty-state.php — icon set, default heading/description, and button defaults.First use case — the one that pays for itself immediately: a paginated resource index where the "no results" branch currently renders an empty <tbody>. Drop the component inside the @forelse's @empty block:
<table>
@forelse ($posts as $post)
<tr>…</tr>
@empty
<x-empty-state
icon="document-text"
heading="No posts yet"
description="Publish your first post to see it listed here."
>
<x-slot:action>
<a href="{{ route('posts.create') }}" class="…">New post</a>
</x-slot:action>
</x-empty-state>
@endforelse
</table>
One component in one table, rendered inside the <td colspan="…">, is a complete first win and validates the install end-to-end.
Two integration shapes. Class-based Blade components (<x-empty-state … />) are the default and what you want in application views. If the package also registers a directive (confirm in the provider), reserve @emptyState(...) for legacy Blade files you don't want to touch, or for string-building inside @php blocks — don't mix both styles in the same file.
Wrap it, don't sprinkle it. Given the package is a zero-star, single-maintainer dependency, the highest-leverage pattern is a thin project-local component:
{{-- resources/views/components/app/empty-state.blade.php --}}
<x-empty-state
:icon="$icon ?? 'inbox'"
:heading="$heading"
:description="$description"
{{ $attributes->class(['py-16']) }}
>
@isset($action) <x-slot:action>{{ $action }}</x-slot:action> @endisset
</x-empty-state>
Every call site becomes <x-app.empty-state> and the third-party API is confined to one file. Replacing or forking the package later is then a one-file edit, not a search-and-replace.
Standardise copy through config, not call sites. Put the recurring messages (No results found, Nothing here yet, No data to import) in config/empty-state.php or a lang/en/empty-states.php file, and reference them at the call site. This keeps tone consistent across the app and gives you a single place to add translations when the app goes multilingual.
Three distinct states — keep them distinct. Empty state (no data exists), no-results state (data exists, filter excluded it), and loading state (skeleton/spinner) are different UX problems. This package handles the first two well. Don't force it into the third; a skeleton should not say "Create your first post."
Typical placements:
@empty branch of an @forelse inside a table's <td colspan><div> where a card grid would render (grid-cols-* wrapper with the component as the sole child)$results->isEmpty() && $hasActiveFilters() — use a shorter description and a "Clear filters" actionLivewire and Inertia. In Livewire, the component is pure Blade, so it renders fine inside a component's view and re-renders on state change — no wiring needed beyond @if ($items->isEmpty()). For Inertia/React/Vue, this package is not your tool; keep the empty state in the JS layer and treat this only as the source of the copy and icon choice.
Testing. assertSee('No posts yet') and assertSeeLivewire-style assertions work as expected. Assert on the copy rather than on markup, so a future package version changing the wrapping element doesn't break your suite.
Tailwind will not see the package's classes. Utilities live in vendor/, outside your content globs, so JIT purges them and the component renders unstyled. Fix it in tailwind.config.js:
content: [
'./resources/**/*.blade.php',
'./vendor/mrshanebarron/empty-state/resources/views/**/*.blade.php',
],
If you publish the views this becomes moot — the published files are inside your scan path. That's often the real reason to publish, not styling control.
The v1.0.2 fix is a naming fix, and naming bugs reappear in caches. Hyphenated component names now resolve to properly PascalCased classes (blog-post → BlogPost). After upgrading from 1.0.1:
php artisan view:clear
php artisan config:clear
composer dump-autoload
If the component silently renders nothing (no exception, no markup), suspect component-class resolution or a stale compiled view before suspecting your data. php artisan view:clear is the first diagnostic, not the last.
Published views are frozen in time. Once published, package bug fixes and markup improvements no longer reach you. Record in the PR description why you published, and diff resources/views/vendor/empty-state/ against the vendor copy on every minor upgrade.
Icon rendering depends on an icon package. If the component resolves icons through Heroicons, Blade Icons, or similar, that package must also be installed and the specific icon name must exist — a missing icon typically renders nothing rather than erroring, which reads as a layout bug. Heroicons v1 and v2 have different name sets (search vs magnifying-glass); pick one and stay there.
Prop vs slot precedence. If you pass both a heading attribute and a heading slot (or vice versa), behaviour depends on the component implementation. Decide on one convention in your wrapper and don't rely on the package's resolution order.
Config caching bites in deploy. Any empty-state defaults changed via config are frozen by php artisan config:cache. Clear and re-cache in your deploy script or production will keep serving the old copy.
Accessibility: if the empty state replaces a table body or a live-updating list, give the wrapper role="status" (or the table a aria-live="polite") so screen readers announce the change after a filter, and don't skip the heading level — an empty-state heading is not automatically an <h2>.
Dependency risk and pinning. Zero stars, single maintainer, one patch release. Pin to ^1.0, keep it out of any published contract, keep the call sites behind x-app.empty-state, and you can swap it for 30 lines of hand-written Blade at any time. That optionality is worth more than the package's feature set.
How can I help you explore Laravel packages today?