`<x-button fab>` is an extended FAB below `medium` and a filled button from there, but a disabled one was still drawn as the FAB below `medium`: the compact FAB's rule set no state of its own, so the button stood greyed out and fixed over the content. M3 never disables a FAB: "if its action is unavailable, remove the FAB entirely". A `fab` given `disabled` (a `<button disabled>`, or a link's `aria-disabled="true"`) now renders `data-md-unavailable`, and button.css draws it `display: none` below `medium`, which also takes it out of the accessibility tree and the Tab order; from `medium` it is the disabled filled button it was. The mark comes from the prop, not from `:disabled`, because a `spinner` puts `disabled` on its button while the action runs, and that FAB is busy rather than unavailable: hidden, it would vanish instead of showing its loading indicator. The docblocks and the skill's button table say so. A browser test renders an enabled fab with a slow spinner action, a disabled one and a disabled link on a 393px window: the disabled two are not rendered, the busy one stays on screen with its indicator, and at 600px both disabled ones are the disabled button again; it fails without the change in Chrome, Firefox and Safari (and in Chrome with `:disabled` alone as the condition, on the busy FAB). A feature test reads the mark and the rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
411 lines
17 KiB
CSS
411 lines
17 KiB
CSS
/*
|
|
* <x-button>: M3 Expressive's buttons — label button, icon button and toggle — and the page's
|
|
* create action that is a FAB on a compact window.
|
|
*
|
|
* Geometry from androidx Compose Material 3's tokens (Apache-2.0), per `data-md-size`:
|
|
*
|
|
* size height label padding gap type icon (label / icon-only) outline square pressed
|
|
* xs 32px 16px 8px label-lg 20 / 20px 1px md sm
|
|
* sm 40px 16px 8px label-lg 20 / 24px 1px md sm
|
|
* md 56px 24px 8px title-md 24 / 24px 1px lg md
|
|
* lg 96px 48px 12px headline-sm 32 / 32px 2px xl lg
|
|
* xl 136px 64px 16px headline-lg 40 / 40px 3px xl lg
|
|
*
|
|
* (ButtonXSmall…XLargeTokens; the xs padding is the 16px Phase D settled on, ACT-09.) An icon button
|
|
* (`data-md-icon-button`) is as tall and `narrow`, `default` or `wide` across by
|
|
* XSmall…XLargeIconButtonTokens: 28/32/40, 32/40/52, 48/56/72, 64/96/128 and 104/136/184px. The
|
|
* views size the glyphs through `<x-icon size>`; `--md-button-icon` carries the same size for the
|
|
* loading indicator that stands in for the glyph while the button's action runs.
|
|
*
|
|
* Shape: round (`--md-sys-shape-corner-full`) by default; `data-md-shape="square"` and a selected
|
|
* button take the size's square corner, a selected square icon button turns round, and every one
|
|
* squares off to the pressed corner under the finger. Those corner rules are wrapped in `:where()`,
|
|
* so they carry no specificity at all: a group's stylesheet (button-group.css, split-button.css)
|
|
* reshapes the buttons inside it with any selector, without a specificity fight.
|
|
*
|
|
* Colour: `data-md-color` picks the roles a variant draws with; the variants set
|
|
* `--md-button-container`, `--md-button-label`, `--md-button-outline` and `--md-button-elevation`,
|
|
* which one rule paints, so the disabled treatment (on-surface at 10% behind 38% text,
|
|
* FilledButtonTokens.DisabledContainerOpacity and DisabledLabelTextOpacity) replaces them with a
|
|
* plain declaration. Primary's tonal button is secondary-container and its standard icon button
|
|
* on-surface-variant, as M3 draws them; the text button's label is primary, as Compose draws it,
|
|
* not the token's on-surface-variant, which its own source marks as wrong; a selected text button
|
|
* takes the tonal container (the library's deliberate deviations, docs/audits/m3-alignment).
|
|
*
|
|
* Motion: corners, padding, margins and `bottom` on the fast spatial spring, colour and shadow on
|
|
* the fast effects spring beside it — a colour must never overshoot (tokens/motion.css).
|
|
*
|
|
* The button renders the foundation's `md-state-layer` and `md-focus-ring`
|
|
* (foundation/interaction.css), and, at `xs` and `sm` where it draws under 48px, `md-touch-target`
|
|
* too; nothing here copies their rules.
|
|
*
|
|
* `data-md-compact-fab`: below `medium` (600px) the button is an extended FAB in the thumb zone —
|
|
* fixed 16px from the end, 56px tall with 16px padding, the large corner, `title-md`,
|
|
* primary-container at elevation 3 (ExtendedFabPrimaryTokens) — lifted clear of a bottom bar and
|
|
* of a snackbar on screen (`--material-bottom-bar`, `--material-snackbar-height`), because M3 puts a
|
|
* snackbar above a FAB and never over one. From `medium` it is the button it was written as.
|
|
* `data-md-unavailable`, a `fab` given `disabled`, is not drawn below `medium`: M3 never shows a
|
|
* disabled FAB, but removes one whose action is unavailable. From `medium` it is the disabled
|
|
* button. `data-md-responsive` on the label hides it below `expanded` (840px).
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './icon.css';
|
|
@import './loading.css';
|
|
@import './tooltip.css';
|
|
|
|
@layer material.components {
|
|
[data-md-button] {
|
|
--md-button-color: var(--md-sys-color-primary);
|
|
--md-button-on-color: var(--md-sys-color-on-primary);
|
|
--md-button-tone: var(--md-sys-color-secondary-container);
|
|
--md-button-on-tone: var(--md-sys-color-on-secondary-container);
|
|
--md-button-tone-selected: var(--md-sys-color-secondary);
|
|
--md-button-on-tone-selected: var(--md-sys-color-on-secondary);
|
|
--md-button-quiet: var(--md-sys-color-on-surface-variant);
|
|
|
|
--md-button-container: transparent;
|
|
--md-button-label: var(--md-button-color);
|
|
--md-button-outline: transparent;
|
|
--md-button-elevation: none;
|
|
|
|
display: inline-flex;
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
justify-content: center;
|
|
block-size: var(--md-button-height);
|
|
background-color: var(--md-button-container);
|
|
color: var(--md-button-label);
|
|
box-shadow: var(--md-button-elevation);
|
|
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);
|
|
}
|
|
|
|
/* Sizes. */
|
|
[data-md-button][data-md-size='xs'] {
|
|
--md-button-height: 32px;
|
|
--md-button-padding: var(--md-sys-measurement-space200);
|
|
--md-button-gap: var(--md-sys-measurement-space100);
|
|
--md-button-icon: 20px;
|
|
--md-button-outline-width: 1px;
|
|
--md-button-square: var(--md-sys-shape-corner-md);
|
|
--md-button-pressed: var(--md-sys-shape-corner-sm);
|
|
--md-button-narrow: 28px;
|
|
--md-button-default: 32px;
|
|
--md-button-wide: 40px;
|
|
}
|
|
|
|
[data-md-button][data-md-size='sm'] {
|
|
--md-button-height: 40px;
|
|
--md-button-padding: var(--md-sys-measurement-space200);
|
|
--md-button-gap: var(--md-sys-measurement-space100);
|
|
--md-button-icon: 20px;
|
|
--md-button-outline-width: 1px;
|
|
--md-button-square: var(--md-sys-shape-corner-md);
|
|
--md-button-pressed: var(--md-sys-shape-corner-sm);
|
|
--md-button-narrow: 32px;
|
|
--md-button-default: 40px;
|
|
--md-button-wide: 52px;
|
|
}
|
|
|
|
[data-md-button][data-md-size='sm'][data-md-icon-button] {
|
|
--md-button-icon: 24px;
|
|
}
|
|
|
|
[data-md-button][data-md-size='md'] {
|
|
--md-button-height: 56px;
|
|
--md-button-padding: var(--md-sys-measurement-space300);
|
|
--md-button-gap: var(--md-sys-measurement-space100);
|
|
--md-button-icon: 24px;
|
|
--md-button-outline-width: 1px;
|
|
--md-button-square: var(--md-sys-shape-corner-lg);
|
|
--md-button-pressed: var(--md-sys-shape-corner-md);
|
|
--md-button-narrow: 48px;
|
|
--md-button-default: 56px;
|
|
--md-button-wide: 72px;
|
|
}
|
|
|
|
[data-md-button][data-md-size='lg'] {
|
|
--md-button-height: 96px;
|
|
--md-button-padding: var(--md-sys-measurement-space600);
|
|
--md-button-gap: 12px;
|
|
--md-button-icon: 32px;
|
|
--md-button-outline-width: 2px;
|
|
--md-button-square: var(--md-sys-shape-corner-xl);
|
|
--md-button-pressed: var(--md-sys-shape-corner-lg);
|
|
--md-button-narrow: 64px;
|
|
--md-button-default: 96px;
|
|
--md-button-wide: 128px;
|
|
}
|
|
|
|
[data-md-button][data-md-size='xl'] {
|
|
--md-button-height: 136px;
|
|
--md-button-padding: var(--md-sys-measurement-space800);
|
|
--md-button-gap: var(--md-sys-measurement-space200);
|
|
--md-button-icon: 40px;
|
|
--md-button-outline-width: 3px;
|
|
--md-button-square: var(--md-sys-shape-corner-xl);
|
|
--md-button-pressed: var(--md-sys-shape-corner-lg);
|
|
--md-button-narrow: 104px;
|
|
--md-button-default: 136px;
|
|
--md-button-wide: 184px;
|
|
}
|
|
|
|
[data-md-button]:not([data-md-icon-button]) {
|
|
gap: var(--md-button-gap);
|
|
padding-inline: var(--md-button-padding);
|
|
}
|
|
|
|
[data-md-button]:not([data-md-icon-button]):is([data-md-size='xs'], [data-md-size='sm']) {
|
|
font: var(--md-sys-typescale-label-lg);
|
|
letter-spacing: var(--md-sys-typescale-label-lg-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-button]:not([data-md-icon-button])[data-md-size='md'] {
|
|
font: var(--md-sys-typescale-title-md);
|
|
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-button]:not([data-md-icon-button])[data-md-size='lg'] {
|
|
font: var(--md-sys-typescale-headline-sm);
|
|
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-button]:not([data-md-icon-button])[data-md-size='xl'] {
|
|
font: var(--md-sys-typescale-headline-lg);
|
|
letter-spacing: var(--md-sys-typescale-headline-lg-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-button][data-md-icon-button] {
|
|
--md-button-width: var(--md-button-default);
|
|
|
|
inline-size: var(--md-button-width);
|
|
|
|
&[data-md-width='narrow'] {
|
|
--md-button-width: var(--md-button-narrow);
|
|
}
|
|
|
|
&[data-md-width='wide'] {
|
|
--md-button-width: var(--md-button-wide);
|
|
}
|
|
}
|
|
|
|
/* Shape, without specificity: a group reshapes the buttons in it. */
|
|
:where([data-md-button]) {
|
|
border-radius: var(--md-sys-shape-corner-full);
|
|
}
|
|
|
|
:where([data-md-button]):where([data-md-shape='square'], [data-md-selected='true']) {
|
|
border-radius: var(--md-button-square);
|
|
}
|
|
|
|
:where([data-md-button]):where([data-md-icon-button][data-md-shape='square'][data-md-selected='true']) {
|
|
border-radius: var(--md-sys-shape-corner-full);
|
|
}
|
|
|
|
:where([data-md-button]):where(:active) {
|
|
border-radius: var(--md-button-pressed);
|
|
}
|
|
|
|
/* Colour roles. */
|
|
[data-md-button][data-md-color='secondary'] {
|
|
--md-button-color: var(--md-sys-color-secondary);
|
|
--md-button-on-color: var(--md-sys-color-on-secondary);
|
|
--md-button-tone: var(--md-sys-color-secondary-container);
|
|
--md-button-on-tone: var(--md-sys-color-on-secondary-container);
|
|
}
|
|
|
|
[data-md-button][data-md-color='tertiary'] {
|
|
--md-button-color: var(--md-sys-color-tertiary);
|
|
--md-button-on-color: var(--md-sys-color-on-tertiary);
|
|
--md-button-tone: var(--md-sys-color-tertiary-container);
|
|
--md-button-on-tone: var(--md-sys-color-on-tertiary-container);
|
|
}
|
|
|
|
[data-md-button][data-md-color='error'] {
|
|
--md-button-color: var(--md-sys-color-error);
|
|
--md-button-on-color: var(--md-sys-color-on-error);
|
|
--md-button-tone: var(--md-sys-color-error-container);
|
|
--md-button-on-tone: var(--md-sys-color-on-error-container);
|
|
}
|
|
|
|
[data-md-button][data-md-color='success'] {
|
|
--md-button-color: var(--md-sys-color-success);
|
|
--md-button-on-color: var(--md-sys-color-on-success);
|
|
--md-button-tone: var(--md-sys-color-success-container);
|
|
--md-button-on-tone: var(--md-sys-color-on-success-container);
|
|
}
|
|
|
|
[data-md-button][data-md-color='warning'] {
|
|
--md-button-color: var(--md-sys-color-warning);
|
|
--md-button-on-color: var(--md-sys-color-on-warning);
|
|
--md-button-tone: var(--md-sys-color-warning-container);
|
|
--md-button-on-tone: var(--md-sys-color-on-warning-container);
|
|
}
|
|
|
|
[data-md-button][data-md-color='info'] {
|
|
--md-button-color: var(--md-sys-color-info);
|
|
--md-button-on-color: var(--md-sys-color-on-info);
|
|
--md-button-tone: var(--md-sys-color-info-container);
|
|
--md-button-on-tone: var(--md-sys-color-on-info-container);
|
|
}
|
|
|
|
/* Every colour but primary selects into the colour itself and is quiet in it. */
|
|
[data-md-button]:not([data-md-color='primary']) {
|
|
--md-button-tone-selected: var(--md-button-color);
|
|
--md-button-on-tone-selected: var(--md-button-on-color);
|
|
--md-button-quiet: var(--md-button-color);
|
|
}
|
|
|
|
/* Variants. */
|
|
[data-md-button][data-md-variant='filled'] {
|
|
--md-button-container: var(--md-button-color);
|
|
--md-button-label: var(--md-button-on-color);
|
|
|
|
&[data-md-selected='false'] {
|
|
--md-button-container: var(--md-sys-color-surface-container);
|
|
--md-button-label: var(--md-sys-color-on-surface-variant);
|
|
}
|
|
}
|
|
|
|
[data-md-button][data-md-variant='tonal'] {
|
|
--md-button-container: var(--md-button-tone);
|
|
--md-button-label: var(--md-button-on-tone);
|
|
|
|
&[data-md-selected='true'] {
|
|
--md-button-container: var(--md-button-tone-selected);
|
|
--md-button-label: var(--md-button-on-tone-selected);
|
|
}
|
|
}
|
|
|
|
[data-md-button][data-md-variant='outlined'] {
|
|
--md-button-label: var(--md-button-quiet);
|
|
--md-button-outline: var(--md-sys-color-outline-variant);
|
|
|
|
border: var(--md-button-outline-width) solid var(--md-button-outline);
|
|
|
|
&[data-md-selected='true'] {
|
|
--md-button-container: var(--md-sys-color-inverse-surface);
|
|
--md-button-label: var(--md-sys-color-inverse-on-surface);
|
|
--md-button-outline: transparent;
|
|
}
|
|
}
|
|
|
|
[data-md-button][data-md-variant='elevated'] {
|
|
--md-button-container: var(--md-sys-color-surface-container-low);
|
|
--md-button-elevation: var(--md-sys-elevation-1);
|
|
|
|
&[data-md-selected='true'] {
|
|
--md-button-container: var(--md-button-color);
|
|
--md-button-label: var(--md-button-on-color);
|
|
}
|
|
}
|
|
|
|
[data-md-button][data-md-variant='text'][data-md-icon-button]:not([data-md-selected='true']) {
|
|
--md-button-label: var(--md-button-quiet);
|
|
}
|
|
|
|
[data-md-button][data-md-variant='text']:not([data-md-icon-button])[data-md-selected='true'] {
|
|
--md-button-container: var(--md-button-tone);
|
|
--md-button-label: var(--md-button-on-tone);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
[data-md-button]:is([data-md-variant='filled'], [data-md-variant='tonal']):hover {
|
|
--md-button-elevation: var(--md-sys-elevation-1);
|
|
}
|
|
|
|
[data-md-button][data-md-variant='elevated']:hover {
|
|
--md-button-elevation: var(--md-sys-elevation-2);
|
|
}
|
|
}
|
|
|
|
/* Disabled: no state layer, no shadow, M3's disabled colours over whatever the variant set. */
|
|
[data-md-button]:is(:disabled, [aria-disabled='true']) {
|
|
box-shadow: none;
|
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
|
}
|
|
|
|
[data-md-button]:is([data-md-variant='filled'], [data-md-variant='tonal'], [data-md-variant='elevated'], [data-md-variant='outlined'][data-md-selected='true']):is(:disabled, [aria-disabled='true']) {
|
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
|
}
|
|
|
|
[data-md-button]:disabled {
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
[data-md-button][aria-disabled='true'] {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* The glyph's place, and the loading indicator that stands in for it. */
|
|
[data-md-button-icon] {
|
|
display: contents;
|
|
}
|
|
|
|
[data-md-button-spinner] {
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
[data-md-button-spinner] > [data-md-loading] {
|
|
inline-size: var(--md-button-icon);
|
|
block-size: var(--md-button-icon);
|
|
color: currentColor;
|
|
}
|
|
|
|
@media (width < 840px) {
|
|
[data-md-button] > [data-md-responsive] {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (width < 600px) {
|
|
[data-md-button][data-md-compact-fab] {
|
|
--md-button-height: 56px;
|
|
--md-button-padding: var(--md-sys-measurement-space200);
|
|
--md-button-gap: var(--md-sys-measurement-space100);
|
|
|
|
position: fixed;
|
|
inset-inline-end: var(--md-sys-measurement-space200);
|
|
bottom: calc(var(--material-bottom-bar, 0px) + var(--material-snackbar-height, 0px) + var(--md-sys-measurement-space200));
|
|
z-index: 30;
|
|
border-radius: var(--md-sys-shape-corner-lg);
|
|
background-color: var(--md-sys-color-primary-container);
|
|
color: var(--md-sys-color-on-primary-container);
|
|
box-shadow: var(--md-sys-elevation-3);
|
|
font: var(--md-sys-typescale-title-md);
|
|
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-button][data-md-compact-fab] [data-md-icon] {
|
|
inline-size: 24px;
|
|
block-size: 24px;
|
|
}
|
|
|
|
/* M3 never disables a FAB: "if its action is unavailable, remove the FAB entirely"
|
|
(docs/reference/m3/components-actions-communication-containment.md § FAB). Not drawn,
|
|
it is out of the accessibility tree and the Tab order too. */
|
|
[data-md-button][data-md-compact-fab][data-md-unavailable] {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|