Files
livewire-material/resources/css/components/fab.css
T
Andreas Reinhold / reiniandClaude Opus 5 1cf4f16c95 Draw the FAB without Tailwind
Plan step 36. <x-fab> renders data-md-fab with data-md-size,
data-md-color, data-md-variant, data-md-extended and, collapsing on
scroll, data-md-collapse-on-scroll with a bound data-md-collapsed; it
passes its glyph's size to <x-icon>. fab.css draws FabBaseline/Medium/
LargeTokens and ExtendedFab*Tokens per size, the container and filled
colours, elevation 3 and 4 on hover, the state layer and focus ring,
and the collapse morph that leaves actions.css.

data-fab becomes data-md-fab, also in navigation.css (the rail flattens
a nested FAB) and toolbar.css (a docked toolbar's FAB), with their tests.
The actions browser test gains the owed collapse-on-scroll test.

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

208 lines
8.4 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. The state layer and focus ring are the declarations of the foundation's
* `md-state-layer` and `md-focus-ring`. `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);
position: relative;
isolation: isolate;
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;
outline: 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);
&::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background-color: currentColor;
opacity: 0;
pointer-events: none;
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
}
@media (hover: hover) {
&:hover {
box-shadow: var(--md-sys-elevation-4);
}
&:hover::before {
opacity: var(--md-sys-state-hover-state-layer-opacity);
}
}
&:focus-visible {
outline: 3px solid var(--md-sys-color-secondary);
outline-offset: 2px;
}
&:focus-visible::before {
opacity: var(--md-sys-state-focus-state-layer-opacity);
}
&:active::before {
opacity: var(--md-sys-state-pressed-state-layer-opacity);
}
}
[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);
}
}