diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md
index 00c3bc2b..bb3087a3 100644
--- a/resources/boost/skills/livewire-material-development/SKILL.md
+++ b/resources/boost/skills/livewire-material-development/SKILL.md
@@ -274,6 +274,8 @@ M3's plain tooltip, standalone around any trigger: `` draws M3's line, `` M3 Expressive's grouped layout — no line, the cluster set 8px off its neighbours with its items 2px apart and its ends rounded. Reach for the divider first (M3: "on web, use dividers to separate items", and it is the only one a scrolling menu may use); reach for the gap for one or two clusters in a menu short enough not to scroll, and never vary the gap. `` takes `label` (optional) and `gap`; a labelled group without `gap` is the plain heading it always was.
+
### ``
A row of ``s: `…`. `connected` sets them 2px apart with small inner corners (a selected toggle rounds fully). Pass the `size` of the buttons inside.
diff --git a/resources/views/components/menu-group.blade.php b/resources/views/components/menu-group.blade.php
index 61c73c91..a7686837 100644
--- a/resources/views/components/menu-group.blade.php
+++ b/resources/views/components/menu-group.blade.php
@@ -1,9 +1,48 @@
-{{-- A labelled group of items in an ``: "Sort by", "Share with". --}}
+{{-- A group of items in an ``, labelled ("Sort by", "Share with"), set apart by a gap, or
+ both.
-@props(['label'])
+ M3 Expressive gives a menu two ways to break its items into clusters, and they are not
+ interchangeable:
-
class('py-1 first:pt-0 last:pb-0') }}>
-
{{ $label }}
+ - ``, a line. M3: "dividers are more subtle and are the right choice for
+ scrollable menus or text-field dropdowns", and "on web, use dividers to separate items".
+ The default answer, and the only one for a menu long enough to scroll.
+ - ``, the Expressive "Grouped" layout: no line, a gap. M3: "gaps are the
+ more expressive way to separate item clusters" — but "limit to one or two gaps per menu",
+ "don't vary gap size", and "never use gaps in a scrollable menu" (unsupported).
- {{ $slot }}
+ A gapped cluster sets its items 2px apart (SegmentedMenuTokens.SegmentedGap = 2dp) and holds
+ them in a box of their own, so the two ends of the cluster round the way the ends of a whole
+ list round and each cluster reads as one block. Clusters stand 8px apart, the same 8px a
+ separator keeps above and below its line, so a menu is the same height whichever it uses.
+ SegmentedMenuTokens' own GroupShape is 8dp where the library's list ends are 12dp; the
+ library's shape wins, so a cluster's ends and a list's ends match. --}}
+
+@props([
+ 'label' => null,
+ 'gap' => false,
+])
+
+@php
+ $attributes = $attributes
+ ->class([
+ 'py-1 first:pt-0 last:pb-0' => ! $gap,
+ 'not-first:mt-2' => $gap,
+ ])
+ ->merge(array_filter([
+ 'role' => 'group',
+ 'aria-label' => $label,
+ ], fn ($value): bool => filled($value)));
+@endphp
+
+
+ @if (filled($label))
+
{{ $label }}
+ @endif
+
+ @if ($gap)
+
{{ $slot }}
+ @else
+ {{ $slot }}
+ @endif
diff --git a/resources/views/showcase/sections/menus.blade.php b/resources/views/showcase/sections/menus.blade.php
index 446a9516..c8330721 100644
--- a/resources/views/showcase/sections/menus.blade.php
+++ b/resources/views/showcase/sections/menus.blade.php
@@ -60,6 +60,37 @@
BLADE,
+ 'Clusters: a gap, or a divider' => <<<'BLADE'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BLADE,
'Icons in their own colour' => <<<'BLADE'
diff --git a/tests/Feature/Components/MenuTest.php b/tests/Feature/Components/MenuTest.php
index 19c9c936..2f7b0da8 100644
--- a/tests/Feature/Components/MenuTest.php
+++ b/tests/Feature/Components/MenuTest.php
@@ -103,6 +103,25 @@ it('separates and labels groups', function () {
->assertSee('role="group" aria-label="Sort by"', false);
});
+it('separates clusters by a gap instead of a divider when asked', function () {
+ $html = (string) $this->blade('');
+
+ // M3 Expressive's grouped layout: 2px between the items of a cluster, 8px between clusters —
+ // the same 8px the divider keeps above and below its line.
+ expect($html)
+ ->toContain('role="group"')
+ ->toContain('not-first:mt-2')
+ ->toContain('space-y-0.5')
+ ->not->toContain('aria-label')
+ ->not->toContain('py-1 first:pt-0')
+ ->and((string) $this->blade(''))
+ ->toContain('aria-label="Then"')
+ ->toContain('space-y-0.5')
+ ->and((string) $this->blade(''))
+ ->toContain('py-1 first:pt-0 last:pb-0')
+ ->not->toContain('space-y-0.5');
+});
+
it('adds icon-class to the leading icon, over its own colour but not over disabled', function () {
$leading = fn (string $html): string => preg_match('/