From 597656c88a3fe0f3976fb9b9cc9605eaab417dd6 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold Date: Mon, 14 Sep 2026 05:59:30 +0200 Subject: [PATCH] Take colour off the overshooting spatial spring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four components animated background-color and color on ease-spatial-fast, whose fast variant peaks at 1.094 — the one thing tokens/motion.css says a colour must never do. Tailwind cannot emit two springs on one element, so a small unlayered file gives shape and size the spatial spring and colour and elevation the effects one in a single property list. Plan step 18, actions.md ACT-19. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components/actions.css | 40 +++++++++++++++++++ resources/css/material.css | 1 + resources/views/components/button.blade.php | 6 ++- resources/views/components/fab-menu.blade.php | 5 ++- resources/views/components/group.blade.php | 5 ++- .../views/components/menu-item.blade.php | 5 ++- 6 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 resources/css/components/actions.css diff --git a/resources/css/components/actions.css b/resources/css/components/actions.css new file mode 100644 index 00000000..d1868614 --- /dev/null +++ b/resources/css/components/actions.css @@ -0,0 +1,40 @@ +/* + * The transitions an action runs between its states — buttons, connected segments, menu items, + * the FAB menu's trigger. + * + * 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 and size on the fast spatial spring, 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. + * + * Unlayered, as the rest of the package's component CSS, so a `transition-*` utility on the call + * site cannot half-undo one. + */ + +@utility state-transition-fast { + transition-property: border-radius, padding, margin, 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-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-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); +} diff --git a/resources/css/material.css b/resources/css/material.css index 4ffefa6c..f997c77a 100644 --- a/resources/css/material.css +++ b/resources/css/material.css @@ -20,6 +20,7 @@ @import './tokens/font.css'; @import './tokens/theme.css'; @import './tokens/state.css'; +@import './components/actions.css'; @import './components/groups.css'; @import './components/list.css'; @import './components/field.css'; diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php index c010c534..02eac82d 100644 --- a/resources/views/components/button.blade.php +++ b/resources/views/components/button.blade.php @@ -9,7 +9,9 @@ `size` is Expressive's scale — `xs` 32px, `sm` 40px (the default), `md` 56px, `lg` 96px, `xl` 136px — each with its own padding, type style and icon size. `shape` is `round` (the default) or `square`; either one squares off towards a smaller corner while pressed, on the - fast spatial spring. + fast spatial spring, while the colours cross on the fast effects spring beside it + (`state-transition-fast`, resources/css/components/actions.css: a colour must never + overshoot). With an `icon` and no label it is an icon button: `width` is `narrow`, `default` or `wide`, `variant="text"` is M3's standard icon button, and the tooltip or label names it for screen @@ -164,7 +166,7 @@ $classes = [ 'group/button state-layer focus-ring inline-flex shrink-0 cursor-pointer select-none items-center justify-center whitespace-nowrap', - 'transition-[border-radius,background-color,color,box-shadow,padding,margin] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast', + 'state-transition-fast', $dimensions, // A composite component (a split button's halves) passes the corners its place needs. $corners ?? $corner.' '.$pressed[$size], diff --git a/resources/views/components/fab-menu.blade.php b/resources/views/components/fab-menu.blade.php index d80f5ee3..f1b86359 100644 --- a/resources/views/components/fab-menu.blade.php +++ b/resources/views/components/fab-menu.blade.php @@ -14,7 +14,8 @@ Livewire, so it stays open through a render of the component around it. FabMenuBaselineTokens (androidx Compose Material 3, Apache-2.0): 56px items, 4px apart, 8px - above the close button. --}} + above the close button. The FAB's corner and colours morph on the two default springs at once + (`state-transition-default`, resources/css/components/actions.css). --}} @props([ 'icon' => 'add', @@ -46,7 +47,7 @@ @if ($label) aria-label="{{ $label }}" @endif @class([ 'group/fab state-layer focus-ring inline-flex size-14 shrink-0 cursor-pointer items-center justify-center rounded-corner-lg shadow-elevation-3 hover:shadow-elevation-4', - 'transition-[border-radius,background-color,color,box-shadow] duration-(--md-sys-motion-spatial-default-duration) ease-spatial-default aria-expanded:rounded-corner-full', + 'state-transition-default aria-expanded:rounded-corner-full', $colours, ]) > diff --git a/resources/views/components/group.blade.php b/resources/views/components/group.blade.php index 1cff7b5a..22c5b4bb 100644 --- a/resources/views/components/group.blade.php +++ b/resources/views/components/group.blade.php @@ -12,7 +12,8 @@ default), `filled` or `outlined`, as for toggle buttons. The segments share the row unless `inline`. An option with `'disabled' => true` greys its own segment. At `xs` and `sm` a segment carries `touch-target` and a 48px minimum width, which M3 asks for by name and tells - you never to reduce. + you never to reduce. Corners move on the spatial spring and colours on the effects one + (`state-transition-fast`). ReStride's props, kept: `label`, `hint`, `hint-class`, `name` (needed with `x-model`, which names no property), `options`, `option-value`, `option-label`; plus `option-icon`, `size`, @@ -79,7 +80,7 @@ @foreach ($options as $option)