/* * The transitions an action runs between its states — buttons, connected segments, menu items, * the FAB menu's trigger, and an extended FAB collapsing to a FAB on scroll. * * M3 has two spring styles and they are not interchangeable: spatial moves things (position, * size, rotation, rounded corners) and overshoots by 9% on the way, effects changes how things * look (colour, opacity, elevation) and is critically damped, because a colour must never * overshoot — tokens/motion.css says so in the library's own words. An element whose corners and * colours both change on a press therefore needs two springs at once, which Tailwind cannot write * (`duration-*` and `ease-*` each emit one value); CSS can, by giving `transition-property` a * list and matching it position for position. * * `state-transition-fast` is the press/hover/select transition of a control the finger lands on: * shape, size and the place it sits on the fast spatial spring (a `fab` button nudges up when a * snackbar arrives under it), colour and elevation on the fast effects spring. * `state-transition-default` is the same pair one step slower, for the bigger morph of a FAB * turning into a close button. * * `popover-transition` is the enter and exit of a menu: M3 asks for a transition that ties the * list to its trigger, so it scales up from its `transform-origin` on the spatial spring while it * fades in on the effects one, and `allow-discrete` keeps `display` and `overlay` alive long * enough for the exit to be seen. * * These are `@utility` declarations, so they compile into Tailwind's utilities layer like any * other class: a `transition-*` utility written beside one on the call site will fight it, and * whichever Tailwind emits last wins. Pass neither. */ @utility state-transition-fast { 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); } @utility state-transition-default { transition-property: border-radius, padding, margin, background-color, color, box-shadow; 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-default-duration), var(--md-sys-motion-effects-default-duration), var(--md-sys-motion-effects-default-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-default), var(--md-sys-motion-effects-default), var(--md-sys-motion-effects-default); } @utility popover-transition { opacity: 0; scale: 0.95; transition-property: opacity, scale, display, overlay; transition-duration: var(--md-sys-motion-effects-fast-duration), var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration), var(--md-sys-motion-spatial-fast-duration); transition-timing-function: var(--md-sys-motion-effects-fast), var(--md-sys-motion-spatial-fast), linear, linear; transition-behavior: allow-discrete; &:popover-open { opacity: 1; scale: 1; } @starting-style { &:popover-open { opacity: 0; scale: 0.95; } } } /* * A FAB menu's items rise into place as the list opens and sink back as it closes. The entry is * `@starting-style` on the item itself; the exit needs the state to be readable while the popover * is on its way out, which is what `:not(:popover-open)` is — the popover keeps `display: flex` * for the length of its own discrete transition, so the items have somewhere to animate to. * * Unlayered (a plain rule, not an `@utility`), so it beats the item's own utilities. */ [data-fab-menu]:not(:popover-open) > * { translate: 0 0.5rem; opacity: 0; } /* * An extended FAB that collapses to a FAB while the page scrolls down (``, resources/js/fab.js sets `data-collapsed`). M3's extended FAB page: it * "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". * * `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 — the size of morph * `state-transition-default` is for — while the label fades on the effects spring. The collapsed * sizes are the FAB's own (FabBaseline/Medium/LargeTokens: 56/80/96px). The extended FAB's * padding already centres the glyph at `sm` (16px + 24px + 16px) 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. * Corners do not change: each size's FAB and extended FAB share one. * * The transition list repeats the FAB's own shadow and colour transitions, which this rule * replaces. Under reduced motion tokens/motion.css takes every duration to zero, so it swaps. */ [data-fab-collapsible] { --fab-collapsed-size: 3.5rem; 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-fab-collapsible][data-fab-size='md'] { --fab-collapsed-size: 5rem; } [data-fab-collapsible][data-fab-size='lg'] { --fab-collapsed-size: 6rem; } [data-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); } [data-fab-label] > span { overflow: hidden; white-space: nowrap; } [data-fab-collapsible][data-collapsed] { min-inline-size: var(--fab-collapsed-size); gap: 0; } [data-fab-collapsible][data-fab-size='lg'][data-collapsed] { padding-inline: 2rem; } /* The label stays in the accessibility tree — clipped and transparent, never `display: none` — so the collapsed FAB keeps the name the extended one had. */ [data-fab-collapsible][data-collapsed] [data-fab-label] { grid-template-columns: 0fr; opacity: 0; }