<x-checkbox> renders data-md-checkbox, its box, tick and dash, and its row, text and errors as data-md-* attributes; the 18px tick takes a size prop and a label-less box md-touch-target (plan step 36). selection.css moves into material.components as the row, text and state layer the three controls share, and each control's drawing goes to its own file: checkbox.css, radio.css, toggle.css, on the state tokens. The radio and the switch take the renamed shared hooks now and lose their layout classes in their own commits. field.js follows data-md-indeterminate. Renaming the switch's data-handle also stops selection.css matching the slider's handles, which it drew 24px above the track. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
120 lines
5.2 KiB
CSS
120 lines
5.2 KiB
CSS
/*
|
|
* What M3's three selection controls share: the checkbox (components/checkbox.css), the radio button
|
|
* (components/radio.css) and the switch (components/toggle.css) — CheckboxTokens, RadioButtonTokens,
|
|
* SwitchTokens, androidx Compose Material 3, Apache-2.0. Each control's stylesheet imports this one.
|
|
*
|
|
* 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:
|
|
*
|
|
* [data-md-selection-row] the <label>: the control and its text, 16px apart; with a hint
|
|
* the control aligns to the first line; `data-md-right` puts the
|
|
* control at the end; `data-md-invalid` when the server has an error
|
|
* [data-md-checkbox-box] | [data-md-radio-button] | [data-md-switch] the control
|
|
* [data-md-selection-text] the label (body-large, on-surface) and the hint under it
|
|
* [data-md-selection-label], [data-md-selection-hint]
|
|
*
|
|
* Every state layer is on-surface when unselected and primary when selected, at M3's hover opacity
|
|
* (only where a pointer can hover) and its focus and press opacities (tokens/state.css); 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 `md-touch-target`
|
|
* (foundation/interaction.css) so its box catches presses over M3's 48px minimum.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@layer material.components {
|
|
[data-md-selection-row] {
|
|
/* The state layer's tint. */
|
|
--selection-ink: var(--md-sys-color-on-surface);
|
|
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--md-sys-measurement-space200);
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
[data-md-selection-row]:has([data-md-selection-hint]) {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
[data-md-selection-row][data-md-right] {
|
|
flex-direction: row-reverse;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
[data-md-selection-row]:has(input:disabled) {
|
|
cursor: default;
|
|
}
|
|
|
|
[data-md-selection-text] {
|
|
min-width: 0;
|
|
}
|
|
|
|
[data-md-selection-label] {
|
|
display: block;
|
|
color: var(--md-sys-color-on-surface);
|
|
font: var(--md-sys-typescale-body-lg);
|
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-selection-hint] {
|
|
display: block;
|
|
margin-top: var(--md-sys-measurement-space25);
|
|
color: var(--md-sys-color-on-surface-variant);
|
|
font: var(--md-sys-typescale-body-sm);
|
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-selection-row]:has(input:disabled) [data-md-selection-label] {
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
|
}
|
|
|
|
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-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);
|
|
}
|
|
|
|
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-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);
|
|
}
|
|
|
|
[data-md-selection-row]:has(input:checked),
|
|
[data-md-selection-row]:has(input:indeterminate) {
|
|
--selection-ink: var(--md-sys-color-primary);
|
|
}
|
|
|
|
[data-md-selection-row][data-md-invalid] {
|
|
--selection-ink: var(--md-sys-color-error);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
[data-md-selection-row]:hover:not(:has(input:disabled)) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
|
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
|
|
}
|
|
}
|
|
|
|
[data-md-selection-row]:has(input:focus-visible) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
|
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
|
|
}
|
|
|
|
[data-md-selection-row]:not(:has(input:disabled)):active :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
|
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
|
|
}
|
|
}
|