colorlibhq/adminlte-symfony
Official AdminLTE 4 integration for Symfony 8.1+ and PHP 8.4+, featuring Bootstrap 5.3, AssetMapper, Twig components, a config-driven sidebar, SSR-safe dark mode, and an optional EasyAdmin theme. Build polished, responsive admin panels fast with an accessible UI that is easy to customize.
The honest first step: this package will not install in a Laravel app. It is a Symfony bundle (AbstractBundle, Flex recipe, config/packages/*.yaml, Twig Components via symfony/ux-twig-component). If you composer require it into Laravel you get a bundle class nothing autoloads and a hard requirement on symfony/framework-bundle 8.1+. There are three legitimate ways a Laravel developer "uses" this:
colorlibhq/adminlte-laravel from the same monorepo family, with the same component inventory and the same config-driven sidebar. That is your day-to-day answer for actually shipping.templates/, templates/components/, and the config schema, then re-implement in Blade. The markup and class names are the real value.Minimal setup if you are in the Symfony codebase:
composer require colorlibhq/adminlte-symfony
# Flex registers AdminLteBundle automatically; otherwise add to config/bundles.php
Then create the config file — and be careful with the filename/spelling, see Gotchas — and define the menu:
# config/packages/admin_lte.yaml
admin_lte:
title: 'Ops'
color_mode: auto
dashboard_route: app_dashboard
menu:
- { label: 'Dashboard', route: 'app_dashboard', icon: 'bi bi-speedometer2' }
- { header: 'MANAGEMENT' }
- { label: 'Users', route: 'app_user_index', icon: 'bi bi-people', permission: 'ROLE_ADMIN' }
Then one controller and one template:
#[Route('/admin', name: 'app_dashboard')]
public function dashboard(): Response
{
return $this->render('admin/dashboard.html.twig');
}
{% extends '@AdminLte/layout.html.twig' %}
{% block page_title %}Dashboard{% endblock %}
{% block body %}
<twig:Adminlte:SmallBox title="150" text="New Orders" icon="bi bi-bag" url="#" />
{% endblock %}
Where to look first, in this order: templates/demo/dashboard.html.twig (the only end-to-end example), templates/layout.html.twig (tells you exactly which blocks are overridable and where the CDN tags live), the config example in the README, then the changelog — which is unusually informative and doubles as a bug list.
First real use case: an internal back-office (users, orders, a couple of CRUD screens) where you want a sidebar, cards, and dark mode without a Node build. That is precisely the bundle's sweet spot: the CDN defaults mean you can render a dashboard in about fifteen minutes.
Treat the config menu as the single source of truth. This is the package's best idea and the one worth copying into Laravel. One nested array drives the sidebar, the active-route highlighting, the ⌘K CommandPalette index, and per-item authorization. In Laravel the direct analogue is a config/navigation.php plus a Menu service and a View::composer/Blade component, rather than hand-writing <x-nav-link> calls on every page:
// app/Support/Menu.php — Laravel port of the same contract
public function items(): array
{
return collect(config('navigation.menu'))
->map(fn ($i) => [...$i, 'active' => $this->isActive($i)])
->filter(fn ($i) => ! isset($i['permission']) || Gate::allows($i['permission']))
->all();
}
Keep the item keys identical (label, header, route, route_params, url, target, icon, badge, badge_class, permission, children) so documentation and muscle memory transfer between the Symfony and Laravel ports.
Extend, don't include, for the shell. The layout is a Twig parent template with blocks (page_title, body, stylesheets, javascripts), while everything inside the page is a component. In Blade that maps cleanly to @extends('layouts.admin') + @section for the chrome and <x-card>/<x-small-box> for content. Resist the urge to make the shell a component — you lose block-level override points for <head> injection.
Components are prop-slot, not data-driven. <twig:Adminlte:Card title="…" variant="primary" outline collapsible>…</twig:Adminlte:Card> — the Card renders its own header and you supply the body as slot content. Same for Timeline + TimelineItem, Accordion + AccordionItem. Note SidebarNavItem is explicitly recursive, which is why nested children in the config work without you writing a loop for each depth level. In Blade, recursive components need @props discipline and a $depth guard; in Laravel the easiest path is a view partial with @include('partials.nav-item', ['item' => $child]).
Dark mode: the flash-free script is the important artifact. It lives in templates/components/layout/_color_mode_script.html.twig and must execute before first paint — set data-bs-theme on <html>, read localStorage, fall back to prefers-color-scheme. In Laravel this means an inline <script> in <head> before @vite and before any deferred CSS:
<script>
(() => {
const s = localStorage.getItem('lte-theme') ?? 'auto';
const d = s === 'auto' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : s;
document.documentElement.setAttribute('data-bs-theme', d);
})();
</script>
The bundle also exposes window.adminlteToggleColorMode() and ships a Stimulus controller (assets/controllers/color-mode_controller.js) as an alternative — pick one, not both, or your toggle will double-fire.
Asset pipeline: CDN for dev, AssetMapper for prod — and never both. The layout's stylesheets/javascripts blocks emit pinned [email protected] CDN tags so the bundle works with zero config. For production you override those blocks in your base template to emit {{ importmap('app') }} instead. In Laravel the equivalent is @vite(['resources/js/app.js']) and a resources/js/app.js that does import '@colorlibhq/adminlte-symfony/adminlte.js' — but note that the Symfony package's JS entry is only meaningful in its own asset graph; for Laravel you'd import 'admin-lte' from npm directly.
EasyAdmin theme = styling overlay, not a shell. The shipped templates/bundles/EasyAdminBundle/layout.html.twig layers AdminLTE CSS/JS onto EasyAdmin's own layout. The README is explicit that a restructured AdminLTE shell for EasyAdmin is "being rolled out incrementally". If you're the Laravel equivalent (Filament, Nova, Backpack), read this as "don't expect a menu-config takeover" — you'll get colours and font, not the sidebar.
The config filename in the README is inconsistent, and only one spelling works. The prose says config/packages/adminlte.yaml in one place and config/packages/admin_lte.yaml in the Configuration section. The working key is admin_lte (Symfony derives it from AdminLteBundle), so the file must be admin_lte.yaml. A wrong filename doesn't always fail loudly — you'll get a default-configured layout with an empty sidebar and spend an hour on it.
Pin your expectations to 0.2.x. Four stars, zero dependents, and the 0.2.1 changelog documents that the EasyAdmin theme had never rendered before that release — it infinitely recursed into itself (fixed with @!EasyAdmin/layout.html.twig) and its JS block was silently dead because the override declared body_javascripts while EasyAdmin renders body_javascript. That's the maturity signal: the core layout/components are exercised by the demo, the integrations are not. Budget review time for anything beyond layout.html.twig + widgets.
Requirements are bleeding edge. PHP 8.4+, Symfony 8.1+. The changelog mentions 7.4 LTS support "once stabilised", i.e. not yet. Also note PHPUnit 13 (dev-only) needs PHP >= 8.4.1 while the package floor is 8.4.0 — a contributor on exactly 8.4.0 hits a wall. For a Laravel team this is mostly moot, but it tells you the maintenance window is narrow and recent.
Route typos in the menu fail silently at runtime. Nothing validates that route: 'app_user_index' exists when the container boots; Twig's path() throws only when that item renders, and if the item is behind a permission you may never see the error locally. Add a smoke test that resolves every menu route through the router:
public function testMenuRoutesExist(): void
{
foreach ($this->flatten($this->menu()) as $item) {
if (isset($item['route'])) {
$this->assertNotNull($this->router->getRouteCollection()->get($item['route']), $item['route']);
}
}
}
permission is a no-op without symfony/security-bundle. The README notes this in passing. If you're prototyping without security installed, every item renders regardless of role and you'll ship a leak. Same lesson for Laravel: make the Gate check unconditional and have it deny by default when auth is absent.
AssetMapper + Bootstrap double-loading. importmap:require bootstrap admin-lte pulls Bootstrap in twice if admin-lte already bundles it (or if you leave the CDN <link> in the stylesheets block while also emitting importmap('app')). Symptoms are subtle — doubled modal backdrops, tooltips offset by a few pixels, data-bs-theme appearing to half-work. Verify by grepping the rendered HTML for bootstrap.min.css and adminlte.min.css count; there should be exactly one of each, from exactly one source.
Twig silently drops blocks the parent never renders. The body_javascripts bug is a general hazard: a {% block %} you define that the parent doesn't {% block %} in its own body simply vanishes with no warning. When something "doesn't load", don't debug the asset — debug whether the block name is right. bin/console debug:twig --filter=layout and a temporary {{ dump(_self.getBlocks()) }} save time.
Palette opt-ins are additive stylesheets, not config. Since core 4.4–4.8 you can add dist/css/adminlte-colors.css (14 extended colours), dist/css/adminlte-colors-v3.css (the 18 AdminLTE 3 colours), data-lte-primary="teal" on <html> to promote a palette colour to Bootstrap primary, data-lte-print="plain" for printable pages, and data-lte-contrast="aa" for WCAG AA on the v3 palette. None of these have a config/packages/admin_lte.yaml equivalent — you add them to the stylesheets block or your <html> attributes yourself. Don't go looking for a config key.
Override templates, don't fork the bundle. Symfony's templates/bundles/AdminLteBundle/… path shadows the bundle's own templates, and inside a bundle template @!AdminLte/… (! = the bundle's own copy) prevents self-recursion. That's the mechanism behind the EasyAdmin fix, and it's the extension point you'll use most: shadow layout.html.twig, swap CDN tags for Vite/importmap, keep everything else upstream.
The CommandPalette is only as good as your menu. ⌘K is indexed from the sidebar config, so items hidden by permission — and items you nested three levels deep with no route — are the ones users will complain are "missing". Flatten deliberately if you want deep pages searchable.
How can I help you explore Laravel packages today?