JSON menu
The application menu is defined in a single JSON file; the vertical menu, the horizontal menu, the mobile drawer and the command palette search are all generated from that one definition. The active page, badges, external links and submenus up to three levels deep are supported. The menu does no authorization checks; the application decides which items are shown.
Basic usage
acun:menu takes its definition from the menu prop: the name of a file under resources/menu, a full file path, a PHP array or a Menu object. The example passes the definition as an array; in an application, use a JSON file. The layouts' nav slot already wraps the menu; when you use it on its own, put it inside a <nav>.
<nav class="flex flex-col gap-0.5">
<acun:menu :menu="[
['section' => 'General'],
['title' => 'Dashboard', 'icon' => 'squares-2x2', 'url' => '#'],
['title' => 'Orders', 'icon' => 'shopping-bag', 'url' => '#orders'],
['title' => 'Reports', 'icon' => 'chart-bar', 'children' => [
['title' => 'Sales report', 'url' => '#sales'],
['title' => 'Customer report', 'url' => '#customers'],
]],
['section' => 'Support'],
['title' => 'Help center', 'icon' => 'lifebuoy', 'url' => 'https://destek.ornek.com', 'target' => '_blank'],
]" />
</nav>
| Prop | Values |
|---|---|
menu | File name | full path | array | Menu object |
JSON file
The file lives under resources/menu/ and lists its items under the menu key. Each item is either a section heading (section) or a link or group with a title. icon is shown only on top-level items.
{
"menu": [
{ "section": "General" },
{ "title": "Home", "icon": "home", "route": "home" },
{ "title": "Orders", "icon": "shopping-bag", "route": "orders.index", "active": "orders.*" },
{ "title": "Reports", "icon": "chart-bar", "badge": true, "children": [
{ "title": "Sales", "route": "reports.sales" },
{ "title": "Archive", "children": [
{ "title": "2025", "url": "/reports/archive/2025" }
] }
] },
{ "section": "Support" },
{ "title": "Help center", "icon": "lifebuoy", "url": "https://destek.ornek.com", "target": "_blank" }
]
}
| Prop | Values |
|---|---|
section | Section heading; hidden when no items remain under it |
title | Display name |
icon | acun:icon name |
Links
route is a Laravel route name, with its parameters given in params; if the route is not defined, the link becomes #. url takes a relative path or a full address. Because menu data can also come from a database, only http, https, mailto and tel addresses and relative paths are accepted; anything else (e.g. javascript:) is rendered as #. "target": "_blank" opens the link in a new tab and does not use wire:navigate.
{
"menu": [
{ "title": "Home", "icon": "home", "route": "home" },
{ "title": "Customer card", "icon": "identification", "route": "customers.show", "params": { "customer": 42 } },
{ "title": "Archive", "icon": "archive-box", "url": "/reports/archive" },
{ "title": "Support line", "icon": "phone", "url": "tel:+908500000000" },
{ "title": "Help center", "icon": "lifebuoy", "url": "https://destek.ornek.com", "target": "_blank" }
]
}
| Prop | Values |
|---|---|
route | Route name |
params | Route parameters |
url | Relative path | http(s) | mailto | tel |
target | _blank |
Active page
Without active, a route item is active while that route is open and a url item is active when the address matches exactly; external addresses are never considered active. To keep an item active on subpages too, give active route names or path patterns starting with / (* is a wildcard). A group that contains the active item is rendered open.
{
"menu": [
{ "title": "Orders", "icon": "shopping-bag", "route": "orders.index", "active": "orders.*" },
{ "title": "Reports", "icon": "chart-bar", "url": "/reports", "active": ["/reports", "/reports/*"] }
]
}
| Prop | Values |
|---|---|
active | Route name | /path pattern | an array of these |
Using it in a layout
Put the menu in the layout's nav slot. horizontal renders a horizontal (top) menu: section headings are skipped and groups become dropdown panels. With the horizontal layout, pass the same file to the mobileNav slot as a vertical menu for the mobile drawer. searchItems() returns every link in the menu for the command palette search, so no separate search list is needed.
{{-- Vertical layout: menu and search from the same file --}}
<acun:layout.sidebar>
<acun:slot:nav><acun:menu menu="sidebar" /></acun:slot:nav>
<acun:slot:search>
<acun:command-palette :items="\Acun\Ui\Menu\Menu::fromFile('sidebar')->searchItems()" />
</acun:slot:search>
…
</acun:layout.sidebar>
{{-- Horizontal layout: horizontal menu on top, vertical menu in the mobile drawer --}}
<acun:layout.horizontal>
<acun:slot:nav><acun:menu menu="sidebar" horizontal /></acun:slot:nav>
<acun:slot:mobileNav><acun:menu menu="sidebar" /></acun:slot:mobileNav>
…
</acun:layout.horizontal>
| Prop | Values |
|---|---|
horizontal | true | false |
Building it in PHP
The menu can also be built in PHP with the Menu class; the definition is an array and can come from, for example, a database. transform is applied to every item before it is rendered; isActive decides whether an item is active, and when it returns null the default rule applies. The menu of this documentation site is built this way.
use Acun\Ui\Menu\Menu;
$menu = new Menu(
$definition,
// Every item passes through here before it is rendered: turn your own field into a link.
transform: fn (array $item) => isset($item['module'])
? ['url' => '/modules/'.$item['module']] + $item
: $item,
// true / false: whether the item is active; null: leave it to the default rule.
isActive: fn (array $item) => isset($item['module']) ? $item['module'] === $activeModule : null,
);
// Blade: <acun:menu :menu="$menu" />
// Command palette: $menu->searchItems()
| Prop | Values |
|---|---|
transform | fn (array $item): array |
isActive | fn (array $item): ?bool |
Props and slots
The values the component accepts.
| Prop | Default | Description |
|---|---|---|
menu | — | Required; a file name (resources/menu/…json), a full path, an array or a Menu object |
horizontal | false | Horizontal (top) menu; section headings are not shown |
sections | auto | In the horizontal menu, true turns each section into a single dropdown (its groups open to the side); false lays the items out side by side; auto uses sections when there are more than 6 items |
section | — | JSON: section heading; top level only |
title | — | JSON: display name |
icon | null | JSON: acun:icon name; shown on the top level |
route | null | JSON: route name; if it is not defined, the link is # |
params | [] | JSON: route parameters |
url | null | JSON: a relative path or an http, https, mailto or tel address; anything else becomes # |
active | null | JSON: a route name or a /path pattern (can be an array, * is a wildcard) |
badge | null | JSON: a number or text; on a group, true = number of child items |
target | null | JSON: _blank = new tab, wire:navigate is not used |
children | — | JSON: child items; up to 3 levels |