diff --git a/resources/css/components/selection.css b/resources/css/components/selection.css
index ade836cf..63306638 100644
--- a/resources/css/components/selection.css
+++ b/resources/css/components/selection.css
@@ -19,6 +19,10 @@
* Every state layer is on-surface when unselected and primary when selected, 8% on hover (only
* where a pointer can hover) and 10% on focus and press; the focus indicator is the package's
* secondary ring. A disabled control is on-surface at 38% and has no state layer.
+ *
+ * The state layer is a ::before with no pointer events, so it is not a target: a labelled control
+ * is pressed anywhere along its row, and a control with no label wears the `touch-target` utility
+ * (resources/css/tokens/state.css) so its box catches presses over M3's 48px minimum.
*/
@layer components {
diff --git a/resources/views/components/checkbox.blade.php b/resources/views/components/checkbox.blade.php
index 6b09cfe7..24a526de 100644
--- a/resources/views/components/checkbox.blade.php
+++ b/resources/views/components/checkbox.blade.php
@@ -6,7 +6,10 @@
partly ticked: HTML has no attribute for it, so the input is marked `data-indeterminate` and
resources/js/field.js keeps the property in step, on load and after every morph. Errors are
read from the bag under the `wire:model` name. Every other attribute reaches the ``
- (resources/css/components/selection.css). --}}
+ (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. --}}
@props([
'label' => null,
@@ -30,7 +33,10 @@
'items-center' => blank($hint),
'flex-row-reverse justify-between' => $right,
])>
- filled($hint)])>
+ filled($hint),
+ 'touch-target' => blank($label) && blank($hint),
+ ])>
except(['class', 'id', 'wire:key']) }}
id="{{ $id }}"
diff --git a/resources/views/components/radio.blade.php b/resources/views/components/radio.blade.php
index 2162c963..f2acb11b 100644
--- a/resources/views/components/radio.blade.php
+++ b/resources/views/components/radio.blade.php
@@ -46,7 +46,10 @@
'items-start' => filled($optionHintText),
'items-center' => blank($optionHintText),
])>
- filled($optionHintText)])>
+ filled($optionHintText),
+ 'touch-target' => blank(data_get($option, $optionLabel)) && blank($optionHintText),
+ ])>
except(['class', 'wire:key', 'name', 'id']) }}
type="radio"
diff --git a/resources/views/components/toggle.blade.php b/resources/views/components/toggle.blade.php
index 93cad8b2..f49e9953 100644
--- a/resources/views/components/toggle.blade.php
+++ b/resources/views/components/toggle.blade.php
@@ -5,7 +5,10 @@
its name — a switch without `label` needs an `aria-label`. `right` puts it at the end of the
row, where a setting's switch usually is. `icons` draws a check on the handle when on and a
cross when off; `icons="selected"` only the check. Every other attribute reaches the ``
- (resources/css/components/selection.css). --}}
+ (resources/css/components/selection.css).
+
+ Without a label the row is only the 52×32 track, so the track carries `touch-target` and catches
+ presses over M3's 48px minimum. --}}
@props([
'label' => null,
@@ -27,7 +30,7 @@
'items-center' => blank($hint),
'flex-row-reverse justify-between' => $right,
])>
-
+ blank($label) && blank($hint)])>
except(['class', 'id', 'wire:key']) }}
id="{{ $id }}"
diff --git a/tests/Feature/Components/SelectionTest.php b/tests/Feature/Components/SelectionTest.php
index 7e8b457e..4b0e1fbe 100644
--- a/tests/Feature/Components/SelectionTest.php
+++ b/tests/Feature/Components/SelectionTest.php
@@ -20,6 +20,17 @@ it('marks a checkbox that is partly ticked', function () {
expect((string) $this->blade(''))->toContain('data-indeterminate');
});
+it('gives a selection control without a label a 48px target', function () {
+ expect((string) $this->blade(''))->toContain('touch-target')
+ ->and((string) $this->blade(''))->toContain('touch-target')
+ ->and((string) $this->blade(''))->toContain('touch-target');
+
+ // A labelled control is pressed anywhere along its row, so it needs no extra target.
+ expect((string) $this->blade(''))->not->toContain('touch-target')
+ ->and((string) $this->blade(''))->not->toContain('touch-target')
+ ->and((string) $this->blade(''))->not->toContain('touch-target');
+});
+
it('puts a checkbox at the end of its row on request', function () {
expect((string) $this->blade(''))->toContain('flex-row-reverse justify-between');
});