hieu-le/active
Laravel helper for detecting the active state of routes URLs and requests, making it easy to highlight the current menu item, tab, or link in your navigation. Provides simple isActive methods and Blade-friendly checks with flexible pattern matching.
Install the package with Composer:
composer require hieu-le/active
On Laravel 5.5+ the service provider and the Active facade are registered by package auto-discovery, so there is no config to publish and no service provider to add manually.
Where to look first
src/HieuLe/Active/ActiveClass.php — the whole package is a single, small class. Reading it end-to-end takes two minutes and tells you exactly how matching works.composer.json of the package to confirm which Laravel versions it was tested against before you add it to a modern app.First use case — a nav link
Once installed, a handful of global helper functions are available in Blade. The typical pattern is: the helper returns a boolean, and active_class() turns that boolean into a class string.
{{-- routes/web.php: Route::get('/', HomeController::class)->name('home'); --}}
<li class="{{ active_class(if_route('home')) }}">
<a href="{{ route('home') }}">Home</a>
</li>
The available helpers are:
| Helper | Purpose |
|---|---|
if_route('users.index') |
Current route name equals the given name |
if_route_pattern('admin.*') |
Current route name matches a * wildcard pattern |
if_action('App\Http\Controllers\UserController@index') |
Current request maps to a controller action |
if_url('/about') |
Current URL matches |
active_class($condition, $active = 'active', $inactive = '') |
Converts a truthy condition into a class string |
If you only ever need named routes, the equivalent native Laravel call is request()->routeIs('home'), which you should keep in the back of your mind as the drop-in replacement (see Gotchas).
Rather than repeating active_class(if_route(...)) everywhere, wrap it once:
{{-- resources/views/components/nav-link.blade.php --}}
@props(['route', 'active' => 'active', 'inactive' => ''])
@php($isActive = if_route_pattern($route))
<a href="{{ route(str_replace('*', '', $route) ?: $route) }}"
class="{{ active_class($isActive, $active, $inactive) }} {{ $attributes->get('class') }}">
{{ $slot }}
</a>
<x-nav-link route="dashboard">Dashboard</x-nav-link>
<x-nav-link route="admin.users.*">Users</x-nav-link>
This is the pattern most teams converge on: the package stays an implementation detail of one component.
if_route_pattern() is where the package earns its keep over plain routeIs in older Laravel versions. Prefix every route in a group and match the group:
Route::prefix('admin')->name('admin.')->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/roles', [RoleController::class, 'index'])->name('roles.index');
});
<li class="{{ active_class(if_route_pattern('admin.*')) }}">Administration</li>
<li class="{{ active_class(if_route_pattern('admin.users.*')) }}">Users</li>
Because admin.users.* matches admin.users.index, admin.users.create, admin.users.edit, the parent link stays highlighted for detail pages — the thing every sidebar needs and hand-rolled code usually gets wrong.
When two routes should light up the same link, combine them with plain PHP — the helpers return booleans:
<li class="{{ active_class(if_route('profile.show') || if_route('profile.edit')) }}">Profile</li>
Same for pattern + exact: if_route('dashboard') || if_route_pattern('reports.*').
If you inherited an app where routes are not named, use if_action() to key off the controller:
<li class="{{ active_class(if_action('App\Http\Controllers\Billing\InvoiceController@index')) }}">Invoices</li>
Prefer route names where you can — action strings are long and break on refactors, whereas route names are stable.
The Active facade is useful in view composers, menu builders, or a NavigationService:
use HieuLe\Active\Facades\Active;
class MenuBuilder
{
public function items(): array
{
return [
['label' => 'Users', 'url' => route('users.index'),
'active' => Active::isActiveRoutePattern('users.*')],
];
}
}
This keeps the "what is active" decision in PHP and lets the Blade partial stay dumb.
Bootstrap, Tailwind, and Bulma all want different class strings. Pass them explicitly instead of hardcoding active:
{{-- Tailwind --}}
<a class="{{ active_class(if_route('home'), 'text-white font-bold', 'text-gray-400') }}">Home</a>
{{-- Bulma --}}
<li class="{{ active_class(if_route('home'), 'is-active') }}">Home</li>
For a menu that should highlight both the top-level section and the deepest item:
<ul>
@foreach ($sections as $section)
<li class="{{ active_class(if_route_pattern($section['prefix'].'.*'), 'open', '') }}">
<a class="{{ active_class(if_route_pattern($section['prefix'].'.*')) }}">{{ $section['label'] }}</a>
<ul>
@foreach ($section['children'] as $child)
<li class="{{ active_class(if_route($child['route'])) }}">...</li>
@endforeach
</ul>
</li>
@endforeach
</ul>
Nothing special is required — assert against the rendered markup:
public function test_users_link_is_active(): void
{
$this->get(route('users.index'))
->assertSee('class="active"', false);
}
Last release March 2022, no maintainer, 408 stars, zero dependents. It has not been run against Laravel 10/11/12. The entire feature set is one class of roughly a hundred lines. For any new project, the native replacement is one line:
{{-- replaces if_route_pattern('admin.*') --}}
<li class="{{ request()->routeIs('admin.*') ? 'active' : '' }}">
request()->routeIs() has supported * wildcards for years, and request()->routeIs() / request()->is() cover the if_url case. If you are inheriting a codebase that uses the package heavily, the cheapest long-term fix is to add these two helpers to app/Support/helpers.php and shadow the package's API rather than rewriting every Blade file:
if (! function_exists('active_class')) {
function active_class(bool $condition, string $active = 'active', string $inactive = ''): string
{
return $condition ? $active : $inactive;
}
}
Then drop the Composer dependency and leave the templates untouched.
Older releases relied on str_is(), which was deprecated in Laravel 5.8 and removed thereafter. If you pin an early version on a modern Laravel you will get a Call to undefined function str_is() at the first wildcard match — and only at the first wildcard match, so it can survive smoke tests. Use the latest release, or avoid wildcards.
nullif_route() compares against request()->route()->getName(). On 404 pages, redirects, closure routes defined without ->name(), or console/queue contexts there is no route at all, and request()->route() is null. In a layout rendered from an error page this throws. Guard it:
{{ active_class(request()->route() && if_route('home')) }}
The Blade-safe habit: always wrap the helper in active_class() (which coerces the value) rather than using if_route() directly in an @if.
if_url('/') is a trapThe root URL matches as a prefix of everything in most implementations, so a "Home" link built with if_url('/') stays permanently active. Always use if_route('home') for the homepage, and reserve if_url() for routes you genuinely cannot name.
if_url() compares against the request path/URL — ?page=2, a trailing slash, or a locale prefix (/en/about) will all cause a miss. If your app is localized with a prefix, strip it before comparing, or (better) name your routes and use if_route().
This is a server-side, full-page-render helper. With Inertia or an SPA router, client-side navigation does not re-render the layout from the server, so the active class will be stale after the first click. In those stacks the active state must be computed in JavaScript from the current URL — the package has nothing to offer there. With Livewire, navigation does go through the server, but only the component re-renders; if your nav is outside the component it will not update. Use wire:navigate with a full page visit, or compute active state inside the Livewire component.
active_class() takes a boolean, not a route nameThe most common mistake is active_class('home') — a non-empty string is truthy, so everything is always active. The correct composition is always active_class(if_route('home')). If you want a one-argument form, write your own wrapper:
function is_active_route(string $route): string
{
return active_class(if_route_pattern($route));
}
The package is stateless and reads only the current request, so the bug is almost always in the route name. Dump the actual value first:
{{-- temporary --}}
<!-- route: {{ optional(request()->route())->getName() ?? 'NONE' }} -->
Nine times out of ten the answer is NONE (unnamed route), a group prefix you forgot (admin.users.index vs users.index), or a wildcard pattern that is broader than intended — if_route_pattern('user*') matches users.index and user-settings.index.
There is no published config file and no container binding you are meant to override. The class is concrete and not designed for swapping. If you need different matching semantics (e.g. ignoring a locale segment, or treating edit/show as the same menu item), you cannot extend it cleanly — define your own helper that delegates to request()->routeIs(), which gives you full control and removes the dependency.
How can I help you explore Laravel packages today?