Skip to content
Brok UI

Loading…

No results

Menus

Open source Core

Action menus — a dropdown on a trigger, a right-click context menu, an application menubar and an inline-expanding nested menu.

Version
v1.0.4
Stability
stable
License
MIT
Related
Dropdown Menu
Context Menu
Menubar

Nested Menu — A hierarchical, inline-expanding menu built on native <details>, rendered recursively for arbitrary depth.

Preview

previews.components.nested-menu.default.blade.php Blade
<div class="w-full flex justify-center py-4">
    <x-ui.nested-menu :items="[
        ['label' => 'New', 'children' => [
            ['label' => 'Document', 'href' => '#'],
            ['label' => 'Folder', 'href' => '#'],
            ['label' => 'From template', 'children' => [
                ['label' => 'Meeting notes', 'href' => '#'],
                ['label' => 'Project brief', 'href' => '#'],
            ]],
        ]],
        ['label' => 'Share', 'children' => [
            ['label' => 'Copy link', 'href' => '#', 'shortcut' => '⌘C'],
            ['label' => 'Invite people', 'href' => '#'],
        ]],
        ['label' => 'Settings', 'href' => '#'],
    ]" />
</div>

Installation

terminal
php artisan ui:add nested-menu

Registry contract

php artisan ui:add nested-menu writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/nested-menu.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

nested-menu.md
# Brok UI: Nested Menu (`nested-menu`)

A hierarchical, inline-expanding menu built on native <details>, rendered recursively for arbitrary depth.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add nested-menu
```

## Usage

```blade
<div class="w-full flex justify-center py-4">
    <x-ui.nested-menu :items="[
        ['label' => 'New', 'children' => [
            ['label' => 'Document', 'href' => '#'],
            ['label' => 'Folder', 'href' => '#'],
            ['label' => 'From template', 'children' => [
                ['label' => 'Meeting notes', 'href' => '#'],
                ['label' => 'Project brief', 'href' => '#'],
            ]],
        ]],
        ['label' => 'Share', 'children' => [
            ['label' => 'Copy link', 'href' => '#', 'shortcut' => '⌘C'],
            ['label' => 'Invite people', 'href' => '#'],
        ]],
        ['label' => 'Settings', 'href' => '#'],
    ]" />
</div>
```

## Props

- `items` (array, default `[…]`) — Tree of menu entries; each item has a label and either an href or a children array for a nested branch.
- `level` (int, default `0`) — Internal recursion depth used to add the guide rail and indent on nested lists; consumers do not set this.

## Use when

- Use for secondary actions when compact access is more important than constant visibility.
- Presenting a deeply nested navigation or command tree that should stay inline rather than opening floating submenus.
- Needing expand/collapse to work with no JavaScript, since each branch is a native details/summary element.

## Avoid when

- Do not hide the most frequent or highest-value actions in a menu when discoverability matters.
- Only one or two shallow levels are needed; use dropdown with a submenu instead of the recursive inline structure.
- The menu should float over the page instead of pushing surrounding content; use dropdown or context-menu.

## Anti-patterns

- Hiding primary actions in menus

## Rules

- Use the `<brok:nested-menu>` tag (or `<x-ui.nested-menu>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/nested-menu
- Registry JSON (files, props, contract): https://brokui.dev/r/open/nested-menu.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
items array […] Tree of menu entries; each item has a label and either an href or a children array for a nested branch.
level int 0 Internal recursion depth used to add the guide rail and indent on nested lists; consumers do not set this.

Slots

  • default —

Data slots

Stable hooks for CSS overrides and browser tests.

nested-menu

Behavior

  • Each branch with children renders as a native details/summary element, so it expands and collapses with the keyboard and assistive tech without any JavaScript.
  • The component renders itself recursively for arbitrary depth, indenting each nested level with a guide rail.
  • The chevron icon is scoped to its own details element and mirrors under dir="rtl", so an open parent never rotates a closed child's chevron.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Secondary action menu

Provide compact access to secondary actions.

Use when

  • Use for secondary actions when compact access is more important than constant visibility.
  • Presenting a deeply nested navigation or command tree that should stay inline rather than opening floating submenus.
  • Needing expand/collapse to work with no JavaScript, since each branch is a native details/summary element.

Avoid when

  • Do not hide the most frequent or highest-value actions in a menu when discoverability matters.
  • Only one or two shallow levels are needed; use dropdown with a submenu instead of the recursive inline structure.
  • The menu should float over the page instead of pushing surrounding content; use dropdown or context-menu.

Use instead

  • Visible button for frequent actions
  • Inline action bar

Anti-patterns

  • Hiding primary actions in menus
Anatomy
root
Theming hooks
nested-menu

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
native
Focus
native
  • Because branches use native details/summary, expand and collapse state, and keyboard operability, come for free from the browser rather than custom ARIA wiring.
  • The root renders as a labelled nav landmark, so assistive tech can jump straight to the menu.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Safe

Livewire can update this component through forwarded wire:* attributes.

Validation

Validation support: native. Keep the error message connected with aria-describedby.

livewire-form.blade.php Blade
<form wire:submit="save" class="space-y-2">
    <brok:nested-menu
        wire:model="value"
        :aria-invalid="$errors->has('value') ? 'true' : 'false'"
        aria-describedby="value-error"
    />

    @error('value')
        <p id="value-error" role="alert">{{ $message }}</p>
    @enderror

    <brok:button type="submit" wire:loading.attr="disabled">
        <span wire:loading.remove>Save</span>
        <span wire:loading>Saving…</span>
    </brok:button>
</form>

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/nested-menu.blade.php Blade
@props([
    'items' => [
        ['label' => 'New', 'children' => [
            ['label' => 'Document', 'href' => '#'],
            ['label' => 'Folder', 'href' => '#'],
            ['label' => 'From template', 'children' => [
                ['label' => 'Meeting notes', 'href' => '#'],
                ['label' => 'Project brief', 'href' => '#'],
            ]],
        ]],
        ['label' => 'Share', 'children' => [
            ['label' => 'Copy link', 'href' => '#'],
            ['label' => 'Invite people', 'href' => '#'],
        ]],
        ['label' => 'Settings', 'href' => '#'],
    ],
    'level' => 0,
])

@php
    $items = is_array($items) ? $items : [];
@endphp

{{--
    Nested menu — a hierarchical, inline-expanding menu. Each branch is a native
    <details>/<summary>, so expand/collapse works with the keyboard and assistive
    tech without any JS; the chevron rotates on open (and mirrors under RTL). The
    component renders itself recursively for arbitrary depth — the `level` prop is
    internal and adds the guide rail / indent on nested lists.
--}}
@if ($level === 0)
<nav
    data-slot="nested-menu"
    aria-label="{{ __('Nested menu') }}"
    {{ $attributes->merge(['class' => 'w-60 max-w-full rounded-md border border-border bg-popover p-1 text-sm text-popover-foreground shadow-md']) }}
>
@endif
    <ul class="space-y-0.5 {{ $level > 0 ? 'ms-3 border-s border-border ps-1' : '' }}">
        @foreach ($items as $item)
            <li>
                @if (! empty($item['children']))
                    {{-- The chevron is scoped to ITS OWN details (direct-child combinator), so an
                         open parent never turns the chevrons of closed children. --}}
                    <details class="[&[open]>summary>svg]:rotate-90">
                        <summary class="flex cursor-pointer list-none items-center gap-2 rounded-md px-2 py-2 transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none [&::-webkit-details-marker]:hidden">
                            <span class="flex-1 truncate">{{ $item['label'] ?? '' }}</span>
                            <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0 text-muted-foreground transition-transform duration-200 rtl:-scale-x-100 motion-reduce:transition-none">
                                <path d="m9 6 6 6-6 6" />
                            </svg>
                        </summary>
                        <x-ui.nested-menu :items="$item['children']" :level="$level + 1" />
                    </details>
                @else
                    <a
                        href="{{ $item['href'] ?? '#' }}"
                        class="flex items-center gap-2 rounded-md px-2 py-2 transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring motion-reduce:transition-none"
                    >
                        <span class="flex-1 truncate">{{ $item['label'] ?? '' }}</span>
                        @if (! empty($item['shortcut']))
                            <kbd class="ms-auto text-xs text-muted-foreground">{{ $item['shortcut'] }}</kbd>
                        @endif
                    </a>
                @endif
            </li>
        @endforeach
    </ul>
@if ($level === 0)
</nav>
@endif

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.0.4
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0