diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md
index 74511648..56e114af 100644
--- a/resources/boost/skills/livewire-material-development/SKILL.md
+++ b/resources/boost/skills/livewire-material-development/SKILL.md
@@ -565,7 +565,7 @@ M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire
M3 selection controls on native inputs; the whole row is the label.
-- `` — `indeterminate` for a "select all" whose items are partly ticked (bind it to a server expression; it follows every render).
+- `` — `indeterminate` for a "select all" whose items are partly ticked (bind it to a server expression; it follows every render). Grouping is yours: from `expanded` (840px) M3 wants a set of related checkboxes gathered into a contained region rather than one long column, so wrap the set in `
` (or a card or side sheet) under a heading that names what the group asks.
- `` — options `['id', 'name', 'hint', 'disabled']` (`option-value`, `option-label`, `option-hint`); `value` checks an option without `wire:model`; `name` names an unbound group. M3 stacks radios and cautions against a row at any width, so reach for `inline` only for two or three short labels; it also wants five options or fewer and one of them chosen when the page loads.
- `` — M3 switch (`role="switch"`); `icons` puts a check and a cross on the handle, `icons="selected"` only the check. Without `label`, pass `aria-label`.
diff --git a/resources/views/components/checkbox.blade.php b/resources/views/components/checkbox.blade.php
index 150d0a28..9aaea1f8 100644
--- a/resources/views/components/checkbox.blade.php
+++ b/resources/views/components/checkbox.blade.php
@@ -10,7 +10,13 @@
(resources/css/components/selection.css).
Without a label — a "select all" in a table header, a row's tick — the row is only the 18px box,
- so the box carries `touch-target` and catches presses over M3's 48px minimum. --}}
+ so the box carries `touch-target` and catches presses over M3's 48px minimum.
+
+ Grouping is the caller's, not this component's: M3 asks that from `expanded` (840px) a set of
+ related checkboxes be gathered into a contained region rather than left as one long column
+ (docs/reference/m3/components-navigation-selection-inputs.md § Checkbox, Behaviour). Lay the
+ group out yourself — `
` around the checkboxes, or a
+ `` or side sheet holding them — and give the group a heading that names what it asks. --}}
@props([
'label' => null,
diff --git a/resources/views/showcase/sections/fields.blade.php b/resources/views/showcase/sections/fields.blade.php
index 8dba5072..0ae1bbed 100644
--- a/resources/views/showcase/sections/fields.blade.php
+++ b/resources/views/showcase/sections/fields.blade.php
@@ -92,6 +92,23 @@
BLADE,
+ 'A checkbox group at expanded' => <<<'BLADE'
+ {{-- M3: from expanded (840px), gather related checkboxes into a contained region instead of one long column. --}}
+
+
+
+ BLADE,
'Radio buttons' => <<<'BLADE'
diff --git a/tests/Feature/Components/SelectionTest.php b/tests/Feature/Components/SelectionTest.php
index d25d5d85..b7e083e7 100644
--- a/tests/Feature/Components/SelectionTest.php
+++ b/tests/Feature/Components/SelectionTest.php
@@ -37,6 +37,29 @@ it('gives a selection control without a label a 48px target', function () {
->and((string) $this->blade(''))->not->toContain('touch-target');
});
+it('leaves a checkbox group to the caller and says how M3 wants it laid out', function () {
+ $html = (string) $this->blade(<<<'BLADE'
+
+ BLADE);
+
+ expect($html)
+ ->toContain('class="grid gap-4 expanded:grid-cols-2"')
+ ->toContain('Downloads')
+ ->toContain('Comments')
+ ->and(substr_count($html, 'data-checkbox'))->toBe(2);
+
+ // The rule is guidance, not markup, so the component's header has to carry it.
+ expect(file_get_contents(__DIR__.'/../../../resources/views/components/checkbox.blade.php'))
+ ->toContain('expanded')
+ ->toContain('contained region');
+});
+
it('puts a checkbox at the end of its row on request', function () {
expect((string) $this->blade(''))->toContain('flex-row-reverse justify-between');
});