@@ -71,15 +71,17 @@ $tile-hover-duration: 450ms;
7171$tile-hover-easing : cubic-bezier (0.22 , 1 , 0.36 , 1 );
7272$tile-rest-duration : 700ms ;
7373
74- // Taking over as the active tile runs in two stages: the tile steps outward, and only then swells
75- // back into its place while the stack behind it reorders. The reference stack steps out by 60 of
76- // its 950, over 190ms, and hops 12 up halfway through the swell.
77- $tile-step-out-distance : math .div (60 , $stack-width ) * 100 cqw; // 6.316cqw
74+ // Taking over as the active tile runs in two stages, quicker and sharper at first, then settling
75+ // in more slowly, and hops 12 up halfway through. A timing split only now, not a position -- see
76+ // the keyframe further down. ARC-3813
7877$tile-step-out-duration : 270ms ;
7978$tile-step-out-easing : cubic-bezier (0.4 , 0 , 0.8 , 0.4 );
8079$tile-settle-lift : 1.2rem ;
8180$tile-activate-duration : $tile-step-out-duration + $tile-rest-duration ; // 690ms
8281$tile-step-out-stop : math .div ($tile-step-out-duration , $tile-activate-duration ) * 100% ; // 27.536%
82+ // Unitless, for the scale blend below: a plain fraction multiplies against a CSS calc() cleanly,
83+ // where a percentage would not.
84+ $tile-step-out-fraction : math .div ($tile-step-out-duration , $tile-activate-duration );
8385$tile-settle-stop : $tile-step-out-stop + 0.45 * (100% - $tile-step-out-stop ); // 60.145%
8486
8587// A border width, not spacing, so not a spacer multiple.
@@ -98,6 +100,16 @@ $pill-height-mobile: variables.$g-spacer-unit * 6;
98100// breakpoint and 1440. The bounds are the mobile and desktop sizes of the design.
99101$pill-font-size : clamp (1.8rem , 1.964 cqw , 2.4rem );
100102
103+ // Registered so the browser treats it as a length, which is what makes it eligible for the
104+ // `transition` below: an unregistered custom property can only snap between values, never ease,
105+ // so a bystander tile losing its shift the instant it becomes the hovered one would otherwise
106+ // jump straight to 0 before the activate keyframe even reads it. ARC-3813
107+ @property --tile-shift {
108+ syntax : " <length>" ;
109+ inherits : true;
110+ initial-value : 0 ;
111+ }
112+
101113.c-driekeuzespeler {
102114 position : relative ;
103115
@@ -186,7 +198,6 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
186198 & :nth-child (1 ) {
187199 --tile-tilt : #{- $tile-tilt } ;
188200 --tile-tilt-active : #{- $tile-active-tilt } ;
189- --tile-step-out : calc (#{$tile-step-out-distance } * -1 );
190201
191202 left : $hit-left-desktop ;
192203 }
@@ -201,7 +212,6 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
201212 & :nth-child (3 ) {
202213 --tile-tilt : #{$tile-tilt } ;
203214 --tile-tilt-active : #{$tile-active-tilt } ;
204- --tile-step-out : #{$tile-step-out-distance } ;
205215
206216 left : $hit-left-desktop + 2 * $tile-offset-desktop ;
207217 }
@@ -251,10 +261,12 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
251261 background-color : var (--tile-color , #{colors .$color-gray-100 } );
252262 box-shadow : $tile-shadow-inactive ;
253263
254- // Transform and shadow only, so no layout work per frame.
264+ // Transform and shadow only, so no layout work per frame. `--tile-shift` rides along so it can
265+ // still be mid-transition when the activate keyframe below starts reading it.
255266 transition :
256267 transform $tile-rest-duration $tile-hover-easing ,
257- box-shadow $tile-rest-duration $tile-hover-easing ;
268+ box-shadow $tile-rest-duration $tile-hover-easing ,
269+ --tile-shift $tile-rest-duration $tile-hover-easing ;
258270
259271 // The white band on the active tile. Always present, revealed only when active, so it does not
260272 // fade in halfway through the grow.
@@ -316,7 +328,7 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
316328 }
317329 }
318330
319- // The tile's tilt and scale on :focus-within is motion, not a focus indicator, and it is gone
331+ // The tile's tilt and scale on :focus-visible is motion, not a focus indicator, and it is gone
320332 // altogether under prefers-reduced-motion. The keyboard user needs a real outline, and it belongs
321333 // on the tile rather than on the zone the button fills, so it hugs what the visitor sees.
322334 & __tile :has (&__tile-button:focus-visible ) & __tile-visual {
@@ -402,9 +414,18 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
402414 }
403415}
404416
405- // Stage one steps the tile outward at its resting size, stage two swells it back into its place.
406- // `--tile-scale` is deliberately not read here: the hover rule below sets it for the tile the
407- // animation holds at the end, so reading it would start the first frame already grown.
417+ // Position moves only via `--tile-shift`. A tile that's already shifted as another tile's
418+ // bystander has its own settle under way on that same property; adding a fixed step-out motion on
419+ // top of it fought over the same axis and produced a visible reversal. One source of motion only,
420+ // so a tile only ever travels towards wherever it currently sits. ARC-3813
421+
422+ // The step-out stop still marks where the easing switches and where scale gets the blended value
423+ // below, rather than following one unbroken curve for the whole animation.
424+
425+ // The tiles overlap at rest (a fanned stack), so z-index cannot go active from frame one: an
426+ // inactive-sized tile would paint over its neighbour before any motion justifies the reorder.
427+ // z-index climbs gradually across this same stage too (the browser interpolates it numerically,
428+ // the same as any other property), so it never gets ahead of the tile's actual position.
408429@keyframes c-driekeuzespeler-tile-activate {
409430 0% {
410431 z-index : $tile-z-inactive ;
@@ -416,8 +437,11 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
416437 #{$tile-step-out-stop } {
417438 // The stack only reorders once the tile is clear of it.
418439 z-index : $tile-z-active ;
419- transform : translate (calc (-50% + var (--tile-shift , 0px ) + var (--tile-step-out , 0px )), -50% )
420- rotate (var (--tile-tilt , 0deg )) scale ($tile-scale-inactive );
440+ transform : translate (calc (-50% + var (--tile-shift , 0px )), -50% )
441+ rotate (var (--tile-tilt , 0deg ))
442+ scale (
443+ calc (#{$tile-scale-inactive } + #{$tile-step-out-fraction } * (var (--tile-scale-active ) - #{$tile-scale-inactive } ))
444+ );
421445 animation-timing-function : $tile-hover-easing ;
422446 }
423447
@@ -449,24 +473,36 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
449473// Every rule below is triggered by the hover zone and lands on the visual inside it. Splitting the
450474// two is what stops the jitter: the zone the pointer tests against never moves.
451475
476+ // `:focus-within` would also stay true after the modal closes: react-modal returns focus to the
477+ // tile that opened it, and a mouse-driven return leaves the tile looking stuck in its hover pose
478+ // while the visitor has moved on. `:focus-visible` only holds the pose for a focus a keyboard
479+ // visitor can see, which is what a returned focus is unless Escape (not the mouse) closed the modal.
480+ // Written out rather than through `:is()`: `:has()` cannot appear inside `:is()`, so every rule
481+ // below spells out the hover and the focus-visible branch as separate, comma-joined selectors.
482+
452483// No hover or focus anywhere in the stack: the middle tile is the active one. `:has` drives the
453484// siblings with no javascript, as BlockHeroCarousel does.
454- .c-driekeuzespeler__tiles :not (:has (.c-driekeuzespeler__tile :is(:hover , :focus-within )))
485+ .c-driekeuzespeler__tiles :not (
486+ :has (.c-driekeuzespeler__tile :hover , .c-driekeuzespeler__tile .c-driekeuzespeler__tile-button :focus-visible )
487+ )
455488 .c-driekeuzespeler__tile :nth-child (2 )
456489 .c-driekeuzespeler__tile-visual {
457490 @include tile-active ;
458491}
459492
460493// A hovered tile takes over the active look whole, so the stack always has one tile at that size.
461- .c-driekeuzespeler__tile :is(:hover , :focus-within ) .c-driekeuzespeler__tile-visual {
494+ .c-driekeuzespeler__tile :hover .c-driekeuzespeler__tile-visual ,
495+ .c-driekeuzespeler__tile :has (:focus-visible ) .c-driekeuzespeler__tile-visual {
462496 @include tile-active ;
463497}
464498
465499// Only a side tile travels: the middle one is already the active tile, and hovering it moves the
466500// two beside it instead. `forwards`, so the tile holds the last frame until the pointer leaves --
467501// the transform transition then carries it back to rest on its own.
468- .c-driekeuzespeler__tile :nth-child (1 ):is(:hover , :focus-within ) .c-driekeuzespeler__tile-visual ,
469- .c-driekeuzespeler__tile :nth-child (3 ):is(:hover , :focus-within ) .c-driekeuzespeler__tile-visual {
502+ .c-driekeuzespeler__tile :nth-child (1 ):hover .c-driekeuzespeler__tile-visual ,
503+ .c-driekeuzespeler__tile :nth-child (1 ):has (:focus-visible ) .c-driekeuzespeler__tile-visual ,
504+ .c-driekeuzespeler__tile :nth-child (3 ):hover .c-driekeuzespeler__tile-visual ,
505+ .c-driekeuzespeler__tile :nth-child (3 ):has (:focus-visible ) .c-driekeuzespeler__tile-visual {
470506 animation :
471507 c- driekeuzespeler- tile- activate $tile-activate-duration linear forwards ,
472508 c- driekeuzespeler- tile- settle $tile-activate-duration linear ;
@@ -475,7 +511,10 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
475511// The stack makes room for the hovered tile: its neighbour steps aside by a nudge, and the tile on
476512// the far side slides inward and drops behind the middle one. Delayed by the first stage, so the
477513// stack waits for the hovered tile to clear it.
478- .c-driekeuzespeler__tiles :has (.c-driekeuzespeler__tile :nth-child (1 ):is(:hover , :focus-within )) {
514+ .c-driekeuzespeler__tiles :has (
515+ .c-driekeuzespeler__tile :nth-child (1 ):hover ,
516+ .c-driekeuzespeler__tile :nth-child (1 ) .c-driekeuzespeler__tile-button :focus-visible
517+ ) {
479518 .c-driekeuzespeler__tile :nth-child (2 ) .c-driekeuzespeler__tile-visual {
480519 --tile-shift : var (--tile-nudge );
481520
@@ -490,7 +529,10 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
490529 }
491530}
492531
493- .c-driekeuzespeler__tiles :has (.c-driekeuzespeler__tile :nth-child (3 ):is(:hover , :focus-within )) {
532+ .c-driekeuzespeler__tiles :has (
533+ .c-driekeuzespeler__tile :nth-child (3 ):hover ,
534+ .c-driekeuzespeler__tile :nth-child (3 ) .c-driekeuzespeler__tile-button :focus-visible
535+ ) {
494536 .c-driekeuzespeler__tile :nth-child (2 ) .c-driekeuzespeler__tile-visual {
495537 --tile-shift : calc (var (--tile-nudge ) * -1 );
496538
@@ -507,7 +549,10 @@ $pill-font-size: clamp(1.8rem, 1.964cqw, 2.4rem);
507549
508550// The middle tile is the active one at rest, so hovering it grows nothing. Both side tiles give it
509551// room instead, which is the only thing that moves.
510- .c-driekeuzespeler__tiles :has (.c-driekeuzespeler__tile :nth-child (2 ):is(:hover , :focus-within )) {
552+ .c-driekeuzespeler__tiles :has (
553+ .c-driekeuzespeler__tile :nth-child (2 ):hover ,
554+ .c-driekeuzespeler__tile :nth-child (2 ) .c-driekeuzespeler__tile-button :focus-visible
555+ ) {
511556 .c-driekeuzespeler__tile :nth-child (1 ) .c-driekeuzespeler__tile-visual {
512557 --tile-shift : calc (var (--tile-push ) * -1 );
513558 }
0 commit comments