Plan step 36 ("Interaction is the shared classes", the user, 2026-09-14).
fab.css hand-rolled the same declarations as foundation/interaction.css's
md-state-layer and md-focus-ring; it now renders those classes and keeps
only its own hover elevation, colour and motion. Every FAB size draws at
56px or more, so it needs no md-touch-target.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
177 lines
7.5 KiB
CSS
177 lines
7.5 KiB
CSS
/*
|
|
* <x-fab>: M3 Expressive's floating action button and extended FAB.
|
|
*
|
|
* FabBaseline/Medium/LargeTokens and ExtendedFab*Tokens (androidx Compose Material 3, Apache-2.0),
|
|
* per `data-md-size`:
|
|
*
|
|
* size FAB corner glyph extended: min width gap padding type
|
|
* sm 56px lg (16px) 24px 80px 8px 16px title-md
|
|
* md 80px lg-increased 28px 80px 16px 26px title-lg
|
|
* lg 96px xl (28px) 32px 96px 20px 28px headline-sm
|
|
*
|
|
* A FAB and its extended form share the height and the corner. `data-md-color` is `primary`,
|
|
* `secondary` or `tertiary`, drawn in its container (`data-md-variant="container"`) or in the
|
|
* colour itself (`filled`), at elevation 3, 4 under a hovering pointer, the shadow and colours on
|
|
* the fast effects spring. It renders the foundation's `md-state-layer` and `md-focus-ring`
|
|
* (foundation/interaction.css); every size draws at 56px or more, so it needs no `md-touch-target`.
|
|
* `data-md-fab` on the root is also the hook a place uses to draw a nested FAB its own way: a
|
|
* navigation rail or a docked toolbar flattens it to elevation 0.
|
|
*
|
|
* `data-md-collapse-on-scroll` is M3's extended FAB that "can collapse to a FAB on scroll-down and
|
|
* re-expand to extended on scroll-up — when switching between FAB↔extended FAB, shape changes, the
|
|
* icon moves left, and the text label fades in/out" (resources/js/fab.js sets
|
|
* `data-md-collapsed`). `width: auto` cannot transition, so the label sits in a one-column grid
|
|
* whose track closes to zero; that track is what animates, and the button lays itself out around
|
|
* it every frame. The gap and the minimum width go with it, all on the default spatial spring,
|
|
* while the label fades on the effects spring. The collapsed width is the FAB's own size. The
|
|
* extended padding already centres the glyph at `sm` (16 + 24 + 16) and `md` (26 + 28 + 26); at
|
|
* `lg` its 28px is 4px short of the 32px that centres a 32px glyph in 96px, so that one moves. The
|
|
* label stays in the accessibility tree — clipped and transparent, never `display: none` — so the
|
|
* collapsed FAB keeps the name the extended one had. Under reduced motion tokens/motion.css takes
|
|
* every duration to zero, so it swaps.
|
|
*/
|
|
|
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
|
|
|
@import './icon.css';
|
|
@import './tooltip.css';
|
|
|
|
@layer material.components {
|
|
[data-md-fab] {
|
|
--md-fab-color: var(--md-sys-color-primary);
|
|
--md-fab-on-color: var(--md-sys-color-on-primary);
|
|
--md-fab-container: var(--md-sys-color-primary-container);
|
|
--md-fab-on-container: var(--md-sys-color-on-primary-container);
|
|
|
|
display: inline-flex;
|
|
flex-shrink: 0;
|
|
align-items: center;
|
|
justify-content: center;
|
|
block-size: var(--md-fab-size);
|
|
border-radius: var(--md-fab-corner);
|
|
background-color: var(--md-fab-container);
|
|
color: var(--md-fab-on-container);
|
|
box-shadow: var(--md-sys-elevation-3);
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
transition-property: box-shadow, background-color, color;
|
|
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
|
transition-timing-function: var(--md-sys-motion-effects-fast);
|
|
|
|
@media (hover: hover) {
|
|
&:hover {
|
|
box-shadow: var(--md-sys-elevation-4);
|
|
}
|
|
}
|
|
}
|
|
|
|
[data-md-fab][data-md-size='sm'] {
|
|
--md-fab-size: 56px;
|
|
--md-fab-corner: var(--md-sys-shape-corner-lg);
|
|
--md-fab-extended-min: 80px;
|
|
}
|
|
|
|
[data-md-fab][data-md-size='md'] {
|
|
--md-fab-size: 80px;
|
|
--md-fab-corner: var(--md-sys-shape-corner-lg-increased);
|
|
--md-fab-extended-min: 80px;
|
|
}
|
|
|
|
[data-md-fab][data-md-size='lg'] {
|
|
--md-fab-size: 96px;
|
|
--md-fab-corner: var(--md-sys-shape-corner-xl);
|
|
--md-fab-extended-min: 96px;
|
|
}
|
|
|
|
[data-md-fab][data-md-color='secondary'] {
|
|
--md-fab-color: var(--md-sys-color-secondary);
|
|
--md-fab-on-color: var(--md-sys-color-on-secondary);
|
|
--md-fab-container: var(--md-sys-color-secondary-container);
|
|
--md-fab-on-container: var(--md-sys-color-on-secondary-container);
|
|
}
|
|
|
|
[data-md-fab][data-md-color='tertiary'] {
|
|
--md-fab-color: var(--md-sys-color-tertiary);
|
|
--md-fab-on-color: var(--md-sys-color-on-tertiary);
|
|
--md-fab-container: var(--md-sys-color-tertiary-container);
|
|
--md-fab-on-container: var(--md-sys-color-on-tertiary-container);
|
|
}
|
|
|
|
[data-md-fab][data-md-variant='filled'] {
|
|
background-color: var(--md-fab-color);
|
|
color: var(--md-fab-on-color);
|
|
}
|
|
|
|
[data-md-fab]:not([data-md-extended]) {
|
|
inline-size: var(--md-fab-size);
|
|
}
|
|
|
|
[data-md-fab][data-md-extended] {
|
|
min-inline-size: var(--md-fab-extended-min);
|
|
}
|
|
|
|
[data-md-fab][data-md-extended][data-md-size='sm'] {
|
|
gap: var(--md-sys-measurement-space100);
|
|
padding-inline: var(--md-sys-measurement-space200);
|
|
font: var(--md-sys-typescale-title-md);
|
|
letter-spacing: var(--md-sys-typescale-title-md-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-fab][data-md-extended][data-md-size='md'] {
|
|
gap: var(--md-sys-measurement-space200);
|
|
padding-inline: 26px;
|
|
font: var(--md-sys-typescale-title-lg);
|
|
letter-spacing: var(--md-sys-typescale-title-lg-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
[data-md-fab][data-md-extended][data-md-size='lg'] {
|
|
gap: 20px;
|
|
padding-inline: 28px;
|
|
font: var(--md-sys-typescale-headline-sm);
|
|
letter-spacing: var(--md-sys-typescale-headline-sm-tracking);
|
|
font-variation-settings: normal;
|
|
}
|
|
|
|
/* The extended FAB that collapses to a FAB while the page scrolls down. */
|
|
[data-md-fab][data-md-collapse-on-scroll] {
|
|
transition-property: min-inline-size, padding-inline, gap, box-shadow, background-color, color;
|
|
transition-duration:
|
|
var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-spatial-default-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-default), var(--md-sys-motion-spatial-default), var(--md-sys-motion-spatial-default),
|
|
var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast), var(--md-sys-motion-effects-fast);
|
|
}
|
|
|
|
[data-md-fab-label] {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
transition-property: grid-template-columns, opacity;
|
|
transition-duration: var(--md-sys-motion-spatial-default-duration), var(--md-sys-motion-effects-default-duration);
|
|
transition-timing-function: var(--md-sys-motion-spatial-default), var(--md-sys-motion-effects-default);
|
|
|
|
& > span {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
[data-md-fab][data-md-collapse-on-scroll][data-md-collapsed] {
|
|
min-inline-size: var(--md-fab-size);
|
|
gap: 0;
|
|
|
|
& [data-md-fab-label] {
|
|
grid-template-columns: 0fr;
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
[data-md-fab][data-md-collapse-on-scroll][data-md-size='lg'][data-md-collapsed] {
|
|
padding-inline: var(--md-sys-measurement-space400);
|
|
}
|
|
}
|