A selection control with a label is pressed anywhere along its row, but one without — a table's "select all", a row's tick — was only its 18, 20 or 32px box, because the 40px state layer is a ::before that catches no pointer. The box now wears the shared touch-target utility, which M3 asks for on all three (checkbox, radio and switch specs: target size 48dp). Plan step 13, finding IN-03. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
349 lines
12 KiB
CSS
349 lines
12 KiB
CSS
/*
|
||
* M3's selection controls: the checkbox, the radio button and the switch (CheckboxTokens,
|
||
* RadioButtonTokens, SwitchTokens, androidx Compose Material 3, Apache-2.0).
|
||
*
|
||
* Each is a real `<input>` with its appearance removed and drawn over, so focus, the keyboard
|
||
* (Space; the arrow keys between radios), the form value, `wire:model` and what a screen reader
|
||
* announces stay the browser's. The row around it is a `<label>`, so its text is pressable too.
|
||
* Hooks are data attributes, not class names: `checkbox`, `radio` and `toggle` are daisyUI's, which
|
||
* the design guard rejects.
|
||
*
|
||
* [data-selection] the <label> row; `data-invalid` when the server has an error for it
|
||
* [data-checkbox] the 18px box, its state layer a 40px circle around it
|
||
* input, [data-check], [data-mixed]
|
||
* [data-radio] the 20px ring and its 10px dot
|
||
* input, [data-dot]
|
||
* [data-switch] the 52×32 track; `data-icons` = "both" or "selected"
|
||
* input, [data-handle] (with [data-on] and [data-off] icons)
|
||
*
|
||
* 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 {
|
||
[data-selection] {
|
||
cursor: pointer;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
[data-selection]:has(input:disabled) {
|
||
cursor: default;
|
||
}
|
||
|
||
[data-selection]:has(input:disabled) [data-selection-label] {
|
||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||
}
|
||
|
||
[data-checkbox],
|
||
[data-radio] {
|
||
position: relative;
|
||
display: grid;
|
||
flex: none;
|
||
place-items: center;
|
||
}
|
||
|
||
[data-checkbox] > *,
|
||
[data-radio] > * {
|
||
grid-area: 1 / 1;
|
||
}
|
||
|
||
[data-checkbox]::before,
|
||
[data-radio]::before,
|
||
[data-handle]::before {
|
||
content: "";
|
||
position: absolute;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
pointer-events: none;
|
||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
[data-checkbox] input,
|
||
[data-radio] input,
|
||
[data-switch] input {
|
||
margin: 0;
|
||
appearance: none;
|
||
cursor: inherit;
|
||
outline: none;
|
||
transition-property: background-color, border-color;
|
||
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
/* The state layer. `--selection-ink` is what it is tinted with. */
|
||
[data-selection] {
|
||
--selection-ink: var(--md-sys-color-on-surface);
|
||
}
|
||
|
||
[data-selection]:has(input:checked),
|
||
[data-selection]:has(input:indeterminate) {
|
||
--selection-ink: var(--md-sys-color-primary);
|
||
}
|
||
|
||
[data-selection][data-invalid] {
|
||
--selection-ink: var(--md-sys-color-error);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
[data-selection]:hover:not(:has(input:disabled)) :is([data-checkbox], [data-radio], [data-handle])::before {
|
||
background-color: color-mix(in srgb, var(--selection-ink) 8%, transparent);
|
||
}
|
||
}
|
||
|
||
[data-selection]:has(input:focus-visible) :is([data-checkbox], [data-radio], [data-handle])::before,
|
||
[data-selection]:not(:has(input:disabled)):active :is([data-checkbox], [data-radio], [data-handle])::before {
|
||
background-color: color-mix(in srgb, var(--selection-ink) 10%, transparent);
|
||
}
|
||
|
||
[data-checkbox]:has(input:focus-visible)::before,
|
||
[data-radio]:has(input:focus-visible)::before {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
}
|
||
|
||
/* ---- Checkbox ---------------------------------------------------------------------------- */
|
||
|
||
[data-checkbox] {
|
||
width: 1.125rem;
|
||
height: 1.125rem;
|
||
}
|
||
|
||
[data-checkbox]::before {
|
||
inset: -0.6875rem;
|
||
}
|
||
|
||
[data-checkbox] input {
|
||
width: 1.125rem;
|
||
height: 1.125rem;
|
||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
[data-checkbox] input:is(:checked, :indeterminate) {
|
||
border-color: var(--md-sys-color-primary);
|
||
background-color: var(--md-sys-color-primary);
|
||
}
|
||
|
||
[data-checkbox] :is([data-check], [data-mixed]) {
|
||
color: var(--md-sys-color-on-primary);
|
||
opacity: 0;
|
||
scale: 0.5;
|
||
pointer-events: none;
|
||
transition-property: opacity, scale;
|
||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||
}
|
||
|
||
[data-checkbox] input:checked:not(:indeterminate) ~ [data-check],
|
||
[data-checkbox] input:indeterminate ~ [data-mixed] {
|
||
opacity: 1;
|
||
scale: 1;
|
||
}
|
||
|
||
[data-selection][data-invalid] [data-checkbox] input {
|
||
border-color: var(--md-sys-color-error);
|
||
}
|
||
|
||
[data-selection][data-invalid] [data-checkbox] input:is(:checked, :indeterminate) {
|
||
background-color: var(--md-sys-color-error);
|
||
}
|
||
|
||
[data-selection][data-invalid] [data-checkbox] :is([data-check], [data-mixed]) {
|
||
color: var(--md-sys-color-on-error);
|
||
}
|
||
|
||
[data-checkbox] input:disabled {
|
||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||
}
|
||
|
||
[data-checkbox] input:disabled:is(:checked, :indeterminate) {
|
||
border-color: transparent;
|
||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||
}
|
||
|
||
[data-checkbox] input:disabled ~ :is([data-check], [data-mixed]) {
|
||
color: var(--md-sys-color-surface);
|
||
}
|
||
|
||
/* ---- Radio button ------------------------------------------------------------------------ */
|
||
|
||
[data-radio] {
|
||
width: 1.25rem;
|
||
height: 1.25rem;
|
||
}
|
||
|
||
[data-radio]::before {
|
||
inset: -0.625rem;
|
||
}
|
||
|
||
[data-radio] input {
|
||
width: 1.25rem;
|
||
height: 1.25rem;
|
||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
}
|
||
|
||
[data-radio] [data-dot] {
|
||
width: 0.625rem;
|
||
height: 0.625rem;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
background-color: var(--md-sys-color-primary);
|
||
scale: 0;
|
||
pointer-events: none;
|
||
transition: scale var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||
}
|
||
|
||
[data-radio] input:checked {
|
||
border-color: var(--md-sys-color-primary);
|
||
}
|
||
|
||
[data-radio] input:checked ~ [data-dot] {
|
||
scale: 1;
|
||
}
|
||
|
||
[data-selection][data-invalid] [data-radio] input {
|
||
border-color: var(--md-sys-color-error);
|
||
}
|
||
|
||
[data-selection][data-invalid] [data-radio] [data-dot] {
|
||
background-color: var(--md-sys-color-error);
|
||
}
|
||
|
||
[data-radio] input:disabled {
|
||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||
}
|
||
|
||
[data-radio] input:disabled ~ [data-dot] {
|
||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||
}
|
||
|
||
/* ---- Switch -------------------------------------------------------------------------------
|
||
*
|
||
* The handle's centre is 16px from the track's start unselected and 36px selected; it is 16px
|
||
* across unselected, 24px with an icon or selected, and 28px while pressed. The positions below
|
||
* are those centres less half the size, measured inside the track's 2px outline. */
|
||
|
||
[data-switch] {
|
||
position: relative;
|
||
display: grid;
|
||
flex: none;
|
||
width: 3.25rem;
|
||
height: 2rem;
|
||
}
|
||
|
||
[data-switch] input {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 2px solid var(--md-sys-color-outline);
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
background-color: var(--md-sys-color-surface-container-highest);
|
||
}
|
||
|
||
[data-switch] input:focus-visible {
|
||
outline: 3px solid var(--md-sys-color-secondary);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
[data-handle] {
|
||
--handle: 1rem;
|
||
|
||
position: absolute;
|
||
top: 50%;
|
||
inset-inline-start: calc(1rem - var(--handle) / 2);
|
||
display: grid;
|
||
place-items: center;
|
||
width: var(--handle);
|
||
height: var(--handle);
|
||
translate: 0 -50%;
|
||
border-radius: var(--md-sys-shape-corner-full);
|
||
background-color: var(--md-sys-color-outline);
|
||
color: var(--md-sys-color-surface-container-highest);
|
||
pointer-events: none;
|
||
transition-property: inset-inline-start, width, height, background-color;
|
||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||
}
|
||
|
||
[data-handle]::before {
|
||
inset: 50% auto auto 50%;
|
||
width: 2.5rem;
|
||
height: 2.5rem;
|
||
translate: -50% -50%;
|
||
}
|
||
|
||
[data-handle] > svg {
|
||
grid-area: 1 / 1;
|
||
opacity: 0;
|
||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||
}
|
||
|
||
[data-switch][data-icons="both"] [data-handle] {
|
||
--handle: 1.5rem;
|
||
}
|
||
|
||
[data-switch][data-icons="both"] input:not(:checked) ~ [data-handle] [data-off],
|
||
[data-switch][data-icons] input:checked ~ [data-handle] [data-on] {
|
||
opacity: 1;
|
||
}
|
||
|
||
[data-switch] input:checked {
|
||
border-color: var(--md-sys-color-primary);
|
||
background-color: var(--md-sys-color-primary);
|
||
}
|
||
|
||
[data-switch] input:checked ~ [data-handle] {
|
||
--handle: 1.5rem;
|
||
|
||
inset-inline-start: calc(2.25rem - var(--handle) / 2);
|
||
background-color: var(--md-sys-color-on-primary);
|
||
color: var(--md-sys-color-on-primary-container);
|
||
}
|
||
|
||
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
|
||
--handle: 1.75rem;
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
[data-selection]:hover [data-switch] input:not(:disabled) ~ [data-handle] {
|
||
background-color: var(--md-sys-color-on-surface-variant);
|
||
}
|
||
|
||
[data-selection]:hover [data-switch] input:checked:not(:disabled) ~ [data-handle] {
|
||
background-color: var(--md-sys-color-primary-container);
|
||
}
|
||
}
|
||
|
||
[data-switch] input:focus-visible ~ [data-handle],
|
||
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
|
||
background-color: var(--md-sys-color-on-surface-variant);
|
||
}
|
||
|
||
[data-switch] input:checked:focus-visible ~ [data-handle],
|
||
[data-selection]:active [data-switch] input:checked:not(:disabled) ~ [data-handle] {
|
||
background-color: var(--md-sys-color-primary-container);
|
||
}
|
||
|
||
[data-switch] input:disabled {
|
||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||
background-color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 12%, transparent);
|
||
}
|
||
|
||
[data-switch] input:disabled ~ [data-handle] {
|
||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||
color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 38%, transparent);
|
||
}
|
||
|
||
[data-switch] input:disabled:checked {
|
||
border-color: transparent;
|
||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||
}
|
||
|
||
[data-switch] input:disabled:checked ~ [data-handle] {
|
||
background-color: var(--md-sys-color-surface);
|
||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||
}
|
||
}
|