arcanedev/laravel-active
Laravel Active provides helpers to detect the current route or URL and mark matching navigation links, menus, and tabs as active, with support for patterns and exclusions. A small utility package by ArcaneDev for building state-aware navigation in Laravel applications.
Install with Composer; Laravel's package discovery registers the service provider automatically:
composer require arcanedev/laravel-active
If you want to change the CSS class it emits (default is active), publish the config:
php artisan vendor:publish
# pick the LaravelActive provider from the list
Then use the global active() helper anywhere a class string is expected — the canonical first use case is a navigation link:
<a href="{{ route('users.index') }}" class="{{ active('users.index') }}">
Users
</a>
When the current route matches users.index, this renders class="active"; otherwise the class attribute is empty. That single behavior is the whole point of the package, so it pays off in layouts with sidebars, top navs, and tab bars.
Where to look first:
config/laravel-active.php after publishing — the only knob most projects touch.src/) — it's small enough to read in a few minutes and tells you exactly which patterns are supported.Start by wiring it into one nav partial, not every link in the app. Once the partial works, expand.
Route-name matching is the primary path. Match a single name, an array of names, or a wildcard prefix:
{{-- single --}}
<a href="{{ route('dashboard') }}" class="{{ active('dashboard') }}">Dashboard</a>
{{-- a resource section: index, create, edit, show all light up together --}}
<a href="{{ route('users.index') }}" class="{{ active(['users.index', 'users.create', 'users.edit']) }}">Users</a>
{{-- wildcard: any route whose name starts with "admin." --}}
<a href="{{ route('admin.dashboard') }}" class="{{ active('admin.*') }}">Admin</a>
The array form is what you want for "section" highlighting where the parent item should stay active while the user is on a child screen.
Path matching for un-named routes. Not every link in a legacy app is a named route, and static/marketing pages often aren't. The package's second capability is matching the current request path, which is its main differentiator over a pure route-name helper:
<a href="/about" class="{{ active('about') }}">About</a>
<a href="/blog" class="{{ active('blog/*') }}">Blog</a>
Keep route-name matching for application screens and path matching for content pages; mixing the two in one nav is fine because the helper is consistent about returning a class string.
Centralize menu definitions. Avoid repeating active() calls across templates. Drive the nav from a config array and let one partial do the matching:
// config/navigation.php
return [
['label' => 'Dashboard', 'route' => 'dashboard', 'match' => 'dashboard'],
['label' => 'Users', 'route' => 'users.index', 'match' => ['users.*']],
['label' => 'Settings', 'route' => 'settings.edit', 'match' => ['settings.*']],
];
@foreach (config('navigation') as $item)
<a href="{{ route($item['route']) }}"
class="{{ active($item['match']) }}"
@if (active($item['match'])) aria-current="page" @endif>
{{ $item['label'] }}
</a>
@endforeach
Note the second active() call for aria-current — the helper returns a string, not a boolean, so use it directly in the attribute when you need truthiness.
Wrap it in a Blade component. Once you have more than a handful of links, a <x-nav-link> component keeps templates clean and gives you one place to change the active class, add aria-current, or swap the helper out later:
{{-- resources/views/components/nav-link.blade.php --}}
@props(['href', 'match'])
@php $isActive = active($match) !== ''; @endphp
<a href="{{ $href }}"
{{ $attributes->class(['active' => $isActive]) }}
@if ($isActive) aria-current="page" @endif>
{{ $slot }}
</a>
This is also the seam you use if you later decide to replace the package with Laravel's native request()->routeIs() / request()->is() — you change the component, not every template.
Combine with authorization/state. Active detection is presentation only; don't let it decide whether a link should exist. Gate the link with @can and let active() decorate it.
Test it like any other view concern. Feature-test the rendered class rather than the helper in isolation:
$this->get(route('users.index'))->assertSee('class="active"', false);
active() yields 'active' or an empty string. That means @if (active(...)) works, but active(...) === true never will, and an unmatched link gives you class="" rather than no attribute. If your CSS or HTML validation cares, use the ternary/!== '' pattern shown above.active('users.index') will not match users.index.edit — use active('users.*'). Conversely, wildcards are greedy: admin.* also matches admin.reports.export, which is usually what you want but can surprise you on nested sections./blog?page=2 and /blog/ may not match a pattern written as blog. Normalize your patterns and test with real URLs, including localized prefixes (/en/blog) if you use mcamara/laravel-localization or similar.users/42/edit needs users/*/edit, not users/edit. Prefer route names anywhere a model is bound.active() is a global function. If another package or your own helpers.php defines it, loading order decides which wins. A quick function_exists('active') check in a scratch route saves an hour of confusion.request()->routeIs('users.*') and request()->is('blog/*'). Keep that in your back pocket.php artisan config:clear (and config:cache in deploy scripts) — a stale cached config is the usual reason a changed active class "doesn't apply."Active:: and get a class-not-found, the alias isn't registered in your app config; either add it or stick to the global helper, which is what most codebases use anyway.active() internally — don't fork the package for it.dd(request()->route()?->getName(), request()->path()) — and compare against your pattern. Nine times out of ten the mismatch is a name typo or an unexpected path prefix, not the package.How can I help you explore Laravel packages today?