Files
Andreas Reinhold / reiniandClaude Opus 5 7803298fe7 Draw the group segment's state layer from the foundation class
Plan step 36 ("Interaction is the shared classes", the user, 2026-09-14).
A segment's hover and press opacities were group.css's own copy of
md-state-layer's declarations; the label now renders md-state-layer
(and, below medium's two smallest sizes, md-touch-target) and the
stylesheet keeps only what the class cannot draw: focus and disabled read
off the real radio or checkbox inside with :has(), not the label itself,
so the focus ring's layer tint and the disabled hide are kept as a
refinement (the latter a real gap the old rule left open). button-group.css,
which only shapes corners and spacing, needed no change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
2026-09-14 18:06:01 +02:00

224 lines
8.9 KiB
CSS

/*
* <x-group>: a choice of a few options drawn as M3 Expressive's connected button group, the
* successor of the segmented button — native radios (checkboxes with `data-md-multiple`) under
* segments that share the row, or hug their labels with `data-md-inline`.
*
* The segments are M3's buttons at the group's `data-md-size` (Button*Tokens: 32/40/56/96/136px
* tall, 16/16/24/48/64px either side, 8/8/8/12/16px between icon and label, label-large, label-
* large, title-medium, headline-small, headline-large), with the connected shape from
* button-group.css: 2px apart, small inner corners, the chosen segment fully round. At `xs` and
* `sm` a segment carries M3's 48px target and a 48px minimum width, which M3 asks for by name.
*
* `data-md-variant` is the toggle button's colours: `tonal` (secondary-container, chosen in
* secondary), `filled` (surface-container behind on-surface-variant, chosen in primary) or
* `outlined` (an outline-variant edge, chosen in inverse-surface). A checked input picks the
* chosen colours (`:has(:checked)`), a disabled one the disabled treatment — on-surface at 10%
* behind 38% text. Corners move on the fast spatial spring and colours on the fast effects spring
* beside it.
*
* A segment renders the foundation's `md-state-layer` (foundation/interaction.css) on the label
* itself, which reaches the same `:hover`/`:active` as the class expects; below `medium`'s smallest
* two sizes it renders `md-touch-target` too. The real control is the `<input>` inside, not the
* label, so keyboard focus and the disabled state are read off it with `:has()` — the one
* difference the class cannot draw, kept here as a refinement.
*
* The legend is label-large and the hint body-small, both in on-surface-variant; a validation
* message takes the hint's place in error. `hint-class` lands on the hint, where a caller's class
* outranks the colour here.
*/
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
@import './icon.css';
@import './button-group.css';
@layer material.components {
[data-md-group] {
min-inline-size: 0;
}
[data-md-group-legend] {
margin-block-end: var(--md-sys-measurement-space100);
color: var(--md-sys-color-on-surface-variant);
font: var(--md-sys-typescale-label-lg);
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
font-variation-settings: normal;
}
[data-md-group] > [data-md-button-group] {
display: flex;
align-items: normal;
inline-size: 100%;
}
[data-md-group][data-md-inline] > [data-md-button-group] {
inline-size: fit-content;
}
[data-md-group-segment] {
--md-group-container: var(--md-sys-color-secondary-container);
--md-group-label: var(--md-sys-color-on-secondary-container);
display: flex;
flex: 1 1 0%;
align-items: center;
justify-content: center;
min-inline-size: 0;
background-color: var(--md-group-container);
color: var(--md-group-label);
white-space: nowrap;
cursor: pointer;
-webkit-user-select: none;
user-select: none;
transition-property: border-radius, padding, margin, bottom, background-color, color, box-shadow;
transition-duration:
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration),
var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration),
var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-effects-fast-duration);
transition-timing-function:
var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast),
var(--md-sys-motion-spatial-fast), var(--md-sys-motion-spatial-fast),
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
/* Focus lands on the input inside, not the label, so it is read with :has(); the state
layer's own opacity joins the ring, as the foundation class draws for a focused control. */
&:has(:focus-visible) {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
&::before {
opacity: var(--md-sys-state-focus-state-layer-opacity);
}
}
/* Disabled is read off the input too: the class's own :disabled/aria-disabled rule never
fires on the label it never reaches. */
&:has(:disabled)::before {
display: none;
}
/* The radio or checkbox stays in the page, for the form, the keyboard and a screen reader. */
& > input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border-width: 0;
}
}
[data-md-group][data-md-inline] [data-md-group-segment] {
flex: initial;
}
[data-md-group]:is([data-md-size='xs'], [data-md-size='sm']) [data-md-group-segment] {
min-inline-size: var(--md-sys-measurement-space600);
}
[data-md-group][data-md-size='xs'] [data-md-group-segment] {
gap: var(--md-sys-measurement-space100);
block-size: 32px;
padding-inline: var(--md-sys-measurement-space200);
font: var(--md-sys-typescale-label-lg);
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
font-variation-settings: normal;
}
[data-md-group][data-md-size='sm'] [data-md-group-segment] {
gap: var(--md-sys-measurement-space100);
block-size: 40px;
padding-inline: var(--md-sys-measurement-space200);
font: var(--md-sys-typescale-label-lg);
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
font-variation-settings: normal;
}
[data-md-group][data-md-size='md'] [data-md-group-segment] {
gap: var(--md-sys-measurement-space100);
block-size: 56px;
padding-inline: var(--md-sys-measurement-space300);
font: var(--md-sys-typescale-title-md);
letter-spacing: var(--md-sys-typescale-title-md-tracking);
font-variation-settings: normal;
}
[data-md-group][data-md-size='lg'] [data-md-group-segment] {
gap: 12px;
block-size: 96px;
padding-inline: var(--md-sys-measurement-space600);
font: var(--md-sys-typescale-headline-sm);
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
font-variation-settings: normal;
}
[data-md-group][data-md-size='xl'] [data-md-group-segment] {
gap: var(--md-sys-measurement-space200);
block-size: 136px;
padding-inline: var(--md-sys-measurement-space800);
font: var(--md-sys-typescale-headline-lg);
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
font-variation-settings: normal;
}
[data-md-group-segment]:has(:checked) {
--md-group-container: var(--md-sys-color-secondary);
--md-group-label: var(--md-sys-color-on-secondary);
}
[data-md-group][data-md-variant='filled'] [data-md-group-segment] {
--md-group-container: var(--md-sys-color-surface-container);
--md-group-label: var(--md-sys-color-on-surface-variant);
&:has(:checked) {
--md-group-container: var(--md-sys-color-primary);
--md-group-label: var(--md-sys-color-on-primary);
}
}
[data-md-group][data-md-variant='outlined'] [data-md-group-segment] {
--md-group-container: transparent;
--md-group-label: var(--md-sys-color-on-surface-variant);
border: 1px solid var(--md-sys-color-outline-variant);
&:has(:checked) {
--md-group-container: var(--md-sys-color-inverse-surface);
--md-group-label: var(--md-sys-color-inverse-on-surface);
border-color: transparent;
}
}
[data-md-group-segment]:has(:disabled) {
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
cursor: not-allowed;
}
[data-md-group-label] {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-md-group-hint],
[data-md-group-error] {
margin-block-start: var(--md-sys-measurement-space50);
font: var(--md-sys-typescale-body-sm);
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
font-variation-settings: normal;
}
[data-md-group-hint] {
color: var(--md-sys-color-on-surface-variant);
}
[data-md-group-error] {
color: var(--md-sys-color-error);
}
}