Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Animating the web | SFI 2023

Animating the web | SFI 2023

Talking about the current state of Web API for animations, and processes required to optimize its performance, including advanced React optimization and browser-specific optimization. Additionally, how animations improve the UX and quality of the product, and how to avoid affecting accessibility.

https://www.youtube.com/watch?v=b0OtzS2b0u0

Bruno Kawka

March 04, 2023
Tweet

More Decks by Bruno Kawka

Other Decks in Programming

Transcript

  1. Frontend Engineer @ Upside • Google Code-In Winner • ACS

    @ UJ kawka.me • github.com/letelete • linkedin.com/in/brunokawka Animating the web Jak nie tracić głowy, kiedy nasze animacje traca klatki? Bruno Kawka ,
  2. Performance issues on low-end devices Motion sickness / Overstimulation /

    Eye fatigue Animations equals Accessibility problem Should I even bother with animations?
  3. Should I even bother with animations? Enhancing the UX Imitating

    the real world Should I even bother with animations?
  4. Guide users over contextful navigation with Hero transitions Should I

    even bother with animations? • Communicate direction of fl ow • Make it clear which elements are related from page to page. • Combine them with data fetching for faster perception of performance. Enhancing the UX https://developer.chrome.com/docs/web-platform/view-transitions
  5. Explain relationship between elements Should I even bother with animations?

    • Inform about the connection between two components Enhancing the UX https://m3.material.io/styles/motion/transitions/applying-transitions
  6. Assure user about the state of the important event with

    Should I even bother with animations? Enhancing the UX https://tympanus.net/Development/ProgressButtonStyles
  7. Describe upcoming changes - Skeletons Enhancing the UX Should I

    even bother with animations? • Hint at where content will appear once it's loaded • Hack perception of time - reduce perceived latency and stabilize layouts as content loads https://m3.material.io/styles/motion/transitions/transition-patterns
  8. Should I even bother with animations? Enhancing the UX Imitating

    the real world Should I even bother with animations?
  9. Transitions Should I even bother with animations? Transitions are fundamental

    to a great user experience because they help users understand how an app works. Well-designed transitions make an experience feel high quality and expressive. Google Material Design
  10. The state of Web APIs The state of Web APIs

    https://developer.chrome.com/docs/web-platform/view-transitions Hero transitions API (The View Transitions API)
  11. View transition API The state of Web APIs const spaNavigate

    = (data) => { document.startViewTransition(() => updateTheDOMSomehow(data)); } Capturing the state Captures current page (takes a screenshot)
  12. View transition API The state of Web APIs const spaNavigate

    = (data) => { document.startViewTransition(() => updateTheDOMSomehow(data)); } Captures current page (takes a screenshot) Add elements / change class names / change styles / … Modifying the DOM
  13. View transition API The state of Web APIs ::view-transition └─

    ::view-transition-group(root) └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) └─ ::view-transition-new(root) Generating pseudo-element tree
  14. View transition API The state of Web APIs ::view-transition └─

    ::view-transition-group(root) └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) └─ ::view-transition-new(root) Generating pseudo-element tree Old captured state, and live representation of the new view
  15. View transition API The state of Web APIs ::view-transition └─

    ::view-transition-group(root) └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) └─ ::view-transition-new(root) Customizing the transition with CSS selectors ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 5s; }
  16. View transition API The state of Web APIs Identifying elements

    across the transition .image { view-transition-name: hero-image; } ├ │ │ │ └─ ::view-transition-group(hero-image) └─ ::view-transition-image-pair(hero-image) ├─ ::view-transition-old(hero-image) └─ ::view-transition-new(hero-image) ::view-transition └─ ::view-transition-group(root) └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) └─ ::view-transition-new(root)
  17. View transition API The state of Web APIs Hero! Hero!

    Hero! .image { view-transition-name: hero-image; } ├ │ │ │ └─ ::view-transition-group(hero-image) └─ ::view-transition-image-pair(hero-image) ├─ ::view-transition-old(hero-image) └─ ::view-transition-new(hero-image) ::view-transition └─ ::view-transition-group(root) └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) └─ ::view-transition-new(root)
  18. View transition API The state of Web APIs Hero! Hero!

    Hero! .image { view-transition-name: hero-image; } ├ │ │ │ └─ ::view-transition-group(hero-image) └─ ::view-transition-image-pair(hero-image) ├─ ::view-transition-old(hero-image) └─ ::view-transition-new(hero-image) ::view-transition └─ ::view-transition-group(root) └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) └─ ::view-transition-new(root) image.onclick = async () => { image.style.viewTransitionName = 'hero-image'; document.startViewTransition(() => { image.style.viewTransitionName = ''; }); };
  19. Intersection Observer API The state of Web APIs Observe changes

    within an intersection of two elements • Lazy-load content as page is scrolled / in fi nite scrolling • Deciding whether to perform tasks or animation processes based on the user perception
  20. Intersection Observer API The state of Web APIs const observer

    = new IntersectionObserver((entries) => { Initialize IntersectionObserver });
  21. Intersection Observer API The state of Web APIs Viewport Observed

    const observer = new IntersectionObserver((entries) => { entries.forEach(({ target, isIntersecting }) => { target.style.backgroundColor = isIntersecting ? 'yellow' : 'transparent'; }); }); const observer = new IntersectionObserver((entries) => { });
  22. Intersection Observer API The state of Web APIs Viewport Observe

    const observer = new IntersectionObserver((entries) => { entries.forEach(({ target, isIntersecting }) => { target.style.backgroundColor = isIntersecting ? 'yellow' : 'transparent'; }); }); const observer = new IntersectionObserver((entries) => { }); observer.observe(document.querySelector('.Observed'));
  23. Intersection Observer API The state of Web APIs const observer

    = new IntersectionObserver((entries) => { entries.forEach(({ target, isIntersecting }) => { target.style.backgroundColor = isIntersecting ? 'yellow' : 'transparent'; }); }); Observed Viewport observer.observe(document.querySelector('.Observed')); });
  24. Intersection Observer API The state of Web APIs const con

    fi g = { threshold: 0.75, }; const observer = new IntersectionObserver((entries) => { entries.forEach(({ target, isIntersecting }) => { target.style.backgroundColor = isIntersecting ? 'yellow' : 'transparent'; }); }, con fi g); observer.observe(document.querySelector('.Observed')); Viewport Observed
  25. Intersection Observer API The state of Web APIs const con

    fi g = { threshold: 0.75, }; const observer = new IntersectionObserver((entries) => { entries.forEach(({ target, isIntersecting }) => { target.style.backgroundColor = isIntersecting ? 'yellow' : 'transparent'; }); }, con fi g); observer.observe(document.querySelector('.Observed')); Observed Viewport 75%
  26. Intersection Observer API The state of Web APIs const con

    fi g = { rootMargin: “-100px”, }; const observer = new IntersectionObserver((entries) => { entries.forEach(({ target, isIntersecting }) => { target.style.backgroundColor = isIntersecting ? 'yellow' : 'transparent'; }); }, con fi g); observer.observe(document.querySelector('.Observed')); Viewport 100px 100px Observed
  27. Intersection Observer API The state of Web APIs const con

    fi g = { rootMargin: “-100px”, }; const observer = new IntersectionObserver((entries) => { entries.forEach(({ target, isIntersecting }) => { target.style.backgroundColor = isIntersecting ? 'yellow' : 'transparent'; }); }, con fi g); observer.observe(document.querySelector('.Observed')); Observed Viewport 100px
  28. Linear interpolation: Clamp The state of Web APIs Given min

    and max, return the closest value within the bounds clamp(24, 20, 30) // 24 clamp(12, 20, 30) // 20 clamp(32, 20, 30) // 30 const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
  29. Linear interpolation: Lerp The state of Web APIs Value between

    two numbers at a specified, decimal midpoint lerp(20, 80, 0); // 20 lerp(20, 80, 1); // 80 lerp(20, 80, 0.5); // 50 const lerp = (min, max, val) => min * (1 - val) + max * val;
  30. Linear interpolation: Inv. Lerp The state of Web APIs Inversed

    Lerp - Decimal midpoint specified between two invlerp(50, 100, 75); // 0.5 invlerp(50, 100, 25); // 0 invlerp(50, 100, 125); // 1 const invlerp = (min, max, val) => clamp((val - min) / (max - min), 0, 1);
  31. Linear interpolation: Range The state of Web APIs Map one

    data range into another range(10, 100, 2000, 20000, 50) // 10000 const range = (min1, max1, min2, max2, val) => { return lerp(min2, max2, invlerp(min1, max1, val)); }
  32. Animatable CSS properties The state of Web APIs A CSS

    property is animatable if its value can be made to change over a given amount of time. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties
  33. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties -moz-outline-radius -moz-outline-radius-bottomleft -moz-outline-radius-bottomright -moz-outline-radius-topleft -moz-outline-radius-topright -ms-grid-columns -ms-grid-rows -webkit-line-clamp -webkit-text-fill-color -webkit-text-stroke -webkit-text-stroke-color accent-color all backdrop-filter background background-color background-position background-size
  34. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties block-size border border-block-end border-block-end-color border-block-end-width border-block-start border-block-start-color border-block-start-width border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-width border-color border-end-end-radius border-end-start-radius border-image-outset border-image-slice
  35. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties border-image-width border-inline-end border-inline-end-color border-inline-end-width border-inline-start border-inline-start-color border-inline-start-width border-left border-left-color border-left-width border-radius border-right border-right-color border-right-width border-start-end-radius border-start-start-radius border-top border-top-color
  36. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties border-top-left-radius border-top-right-radius border-top-width border-width bottom box-shadow caret caret-color caret-shape clip clip-path color column-count column-gap column-rule column-rule-color column-rule-width column-width
  37. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties columns contain-intrinsic-block-size contain-intrinsic-height contain-intrinsic-inline-size contain-intrinsic-size contain-intrinsic-width filter flex flex-basis flex-grow flex-shrink font font-size font-size-adjust font-stretch font-variation-settings font-weight gap
  38. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties grid-column-gap grid-gap grid-row-gap grid-template-columns grid-template-rows height inline-size input-security inset inset-block inset-block-end inset-block-start inset-inline inset-inline-end inset-inline-start left letter-spacing line-clamp
  39. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties line-height margin margin-block margin-block-end margin-block-start margin-bottom margin-inline margin-inline-end margin-inline-start margin-left margin-right margin-top mask mask-border mask-position mask-size max-block-size max-height
  40. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties max-inline-size max-lines max-width min-block-size min-height min-inline-size min-width object-position offset offset-anchor offset-distance offset-path offset-position offset-rotate opacity order outline outline-color
  41. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties outline-offset outline-width padding padding-block padding-block-end padding-block-start padding-bottom padding-inline padding-inline-end padding-inline-start padding-left padding-right padding-top perspective perspective-origin right rotate row-gap
  42. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties scale scroll-margin scroll-margin-block scroll-margin-block-end scroll-margin-block-start scroll-margin-bottom scroll-margin-inline scroll-margin-inline-end scroll-margin-inline-start scroll-margin-left scroll-margin-right scroll-margin-top scroll-padding scroll-padding-block scroll-padding-block-end scroll-padding-block-start scroll-padding-bottom scroll-padding-inline
  43. A CSS property is animatable if its value can be

    made to change over a given amount of time. Animatable CSS properties The state of Web APIs https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties scroll-padding-inline-end scroll-padding-inline-start scroll-padding-left scroll-padding-right scroll-padding-top scroll-snap-coordinate scroll-snap-destination scroll-timeline scrollbar-color shape-image-threshold shape-margin shape-outside tab-size text-decoration text-decoration-color text-decoration-thickness text-emphasis text-emphasis-color
  44. Animatable CSS properties The state of Web APIs text-indent text-shadow

    text-underline-offset top transform transform-origin translate vertical-align visibility width word-spacing z-index zoom A CSS property is animatable if its value can be made to change over a given amount of time. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties
  45. .class { transition: <property> <duration> <timing-function> <delay>; will-change: <animateable-feature>; ⚠

    } Animatable CSS properties The state of Web APIs Just use transition.
  46. .class { transition: <property> <duration> <timing-function> <delay>; will-change: <animateable-feature>; ⚠

    } Animatable CSS properties The state of Web APIs Just use transition. Unless… justify-content 😰
  47. Animating layout changes The state of Web APIs Problem: Recalculating

    layout positions in every frame 60 fps = 60 recalculations per second 60 fps = 16.7 milliseconds to recalculate styles, layout, and paint (1000ms / 60fps ~= 16.7)
  48. The rendering waterfall Recalculate Layou Paint Takes DOM Tree, and

    CSS styles Tree Creates Render Tree The state of Web APIs
  49. The rendering waterfall Recalculate Layou Paint Revalidates parts of render

    tree. Calculates node dimensions. A significant cost to performance. The state of Web APIs
  50. The rendering waterfall The state of Web APIs Recalculate Layou

    Pain Triggers whenever something changed its visibility or appearance.
  51. The rendering waterfall The state of Web APIs Recalculate Layou

    Paint .class { transition: width <duration> <timing-function> <delay>; }
  52. The rendering waterfall The state of Web APIs Recalculate Layou

    Paint .class { transition: width <duration> <timing-function> <delay>; }
  53. The rendering waterfall The state of Web APIs Recalculate Layou

    Paint .class { transition: color <duration> <timing-function> <delay>; }
  54. The rendering waterfall The state of Web APIs Recalculate Layou

    Paint .class { transition: color <duration> <timing-function> <delay>; }
  55. The rendering waterfall The state of Web APIs Recalculate Layou

    Paint .class { transition: transform <duration> <timing-function> <delay>; }
  56. The rendering waterfall The state of Web APIs Recalculate Layou

    Paint .class { transition: transform <duration> <timing-function> <delay>; }
  57. Cheap animatable properties The state of Web APIs transform opacity

    scale Recalculate Layou Paint No repaint triggered. Their updates are handled in the composition.
  58. Animating layout changes The state of Web APIs Problem: Recalculating

    layout positions in every frame 60 fps = 60 recalculations per second 60 fps = 16.7 milliseconds to recalculate styles, layout, and paint
  59. Animating layout changes The state of Web APIs Problem: Recalculating

    layout positions in every frame 60 fps = 60 recalculations per second 60 fps = 16.7 milliseconds to recalculate styles, layout, and paint Solution: Use properties that are rendered in their own layer Don’t trigger repaint, and layout recalculation
  60. The state of Web APIs Animating layout changes Do expensive

    work cheaply using the FLIP technique (First Last Invert Play)
  61. x: 434 y: 262.5 The state of Web APIs How

    FLIP works? First - the initial state of the element involved in the transition .container { display: fl ex; gap: 1rem; }
  62. The state of Web APIs How FLIP works? First -

    the initial state of the element involved in the transition x1: 216 y1: 0 .container { display: fl ex; gap: 1rem; }
  63. The state of Web APIs How FLIP works? Last -

    the final state of the element x1: 216 y1: 0 x2: 0 y2: 216 } fl ex-direction: column; .container { display: fl ex; gap: 1rem;
  64. Inverse - transform element to its initial (First) position using

    cheap CSS animatable properties The state of Web APIs How FLIP works? } fl ex-direction: column; .container { display: fl ex; gap: 1rem; transform: translateX x1: 216 y1: 0 x2: 0 y2: 216 (x1 - x2, y1 - y2)
  65. Inverse - transform element to its initial (First) position using

    cheap CSS animatable properties The state of Web APIs How FLIP works? x1: 216 y1: 0 x2: 0 y2: 216 } fl ex-direction: column; .container { display: fl ex; gap: 1rem; transform: translateX (216, -216)
  66. Play - switch on transitions for changed properties and remove

    the inversion changes The state of Web APIs How FLIP works? } x1: 216 y1: 0 x2: 0 y2: 216 transition: transform 0.5s ease; fl ex-direction: column; .container { display: fl ex; gap: 1rem; transform: translateX (216, -216)
  67. Play - switch on transitions for changed properties and remove

    the inversion changes The state of Web APIs How FLIP works? x1: 216 y1: 0 x2: 0 y2: 216 } transition: transform 0.5s ease; fl ex-direction: column; .container { display: fl ex; gap: 1rem;
  68. Use scale to perform analogical transformations. Compare First and Last

    with centered transform origins The state of Web APIs FLIP vs geometry
  69. Use scale to perform analogical transformations. Compare First and Last

    with centered transform origins The state of Web APIs FLIP vs geometry
  70. Use scale to perform analogical transformations. Compare First and Last

    with centered transform origins The state of Web APIs FLIP vs geometry
  71. When scaling, children will enlarge due to parent transformation. To

    avoid it, on every frame, scale children relatively to the parent The state of Web APIs FLIP: Beware of child distortions Child Child
  72. When scaling, children will enlarge due to parent transformation. To

    avoid it, on every frame, scale children relatively to the parent The state of Web APIs FLIP: Beware of child distortions Child Child Children scales down relatively to the parent scaling up childScale = 1 / parentScale
  73. When scaling, children will enlarge due to parent transformation. To

    avoid it, on every frame, scale children relatively to the parent The state of Web APIs FLIP: Beware of child distortions Child Child Children scales down relatively to the parent scaling up 48pt 48pt childScale = 1 / parentScale
  74. You probably need libraries. The state of Web APIs Framer-motion

    GSAP Spring Lottie Rive Three.js (WebGL) HeadlessUI (Tailwind)
  75. Framer-motion, GSAP, … The state of Web APIs You’ll generally

    use one of these. Great for micro-interactions, orchestration, transitions, timeline-based animations
  76. Lottie, Rive The state of Web APIs After Effects (or

    Rive) animations in real time, controlled with code
  77. Lottie, Rive The state of Web APIs After Effects (or

    Rive) animations in real time, controlled with code
  78. The state of Web APIs Output Reference export const Button

    = ({ onClick }) => { return ( <button onClick={onClick}> <label>{'play'}</label> </button> ); }; Three.js (WebGL)
  79. The state of Web APIs Output Reference export const Button

    = ({ onClick }) => { const [isHovering, setIsHovering] = useState(true); const { transform } = useMemo( () => ({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }), [isHovering] ); const handleMouseEnter = (e) => { e.preventDefault(); setIsHovering(true); }; const handleMouseLeave = (e) => { e.preventDefault(); setIsHovering(false); }; return ( <button onClick={onClick}> <div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={{ transform }} /> <label style={{ transform }}>{‘play'}</label> <div style={{ transform }} /> // shadows </button> ); }; Three.js (WebGL)
  80. The state of Web APIs Output Reference import { useSpring,

    animated } from ‘@react-spring/web'; export const Button = ({ onClick }) => { const [isHovering, setIsHovering] = useState(true); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)', }); const handleMouseEnter = (e) => { e.preventDefault(); setIsHovering(true); }; const handleMouseLeave = (e) => { e.preventDefault(); setIsHovering(false); }; return ( <button onClick={onClick}> <animated.div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={{ transform }} /> <animated.label style={{ transform }}>{‘play'}</label> <animated.div style={{ transform }} /> // shadows </button> ); }; Three.js (WebGL)
  81. The state of Web APIs Output Reference import { useSpring,

    animated } from ‘@react-spring/web'; export const Button = ({ onClick }) => { const [isHovering, setIsHovering] = useState(true); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)', }); const handleMouseEnter = (e) => { e.preventDefault(); setIsHovering(true); }; const handleMouseLeave = (e) => { e.preventDefault(); setIsHovering(false); }; return ( <button onClick={onClick}> <animated.div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} style={{ transform }} /> <ButtonCanvas /> <animated.label style={{ transform }}>{‘play'}</label> <animated.div style={{ transform }} /> // shadows </button> ); }; Three.js: The scene (where you arrange objects) ButtonCanvas
  82. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> </Canvas> ); Three.js: The scene (where you arrange objects)
  83. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The meshes (objects present in your scene) Sphere
  84. The state of Web APIs Output Reference const states =

    { collapsed: { position: [-0.5, -0.8, -1], opacity: 0, }, expanded: { position: [-1, -1.25, 1], opacity: 1, }, }; Three.js: The meshes (objects present in your scene)
  85. The state of Web APIs Output Reference const states =

    { collapsed: { position: [-0.5, -0.8, -1], opacity: 0, }, expanded: { position: [-1, -1.25, 1], opacity: 1, }, }; Three.js: The meshes (objects present in your scene) export const Sphere = () => { const { isHovering } = useContext(ButtonContext); };
  86. The state of Web APIs Output Reference const states =

    { collapsed: { position: [-0.5, -0.8, -1], opacity: 0, }, expanded: { position: [-1, -1.25, 1], opacity: 1, }, }; Three.js: The meshes (objects present in your scene) export const Sphere = () => { const { isHovering } = useContext(ButtonContext); }; const { position } = useSpring( isHovering ? states.expanded : states.collapsed );
  87. The state of Web APIs Output Reference const states =

    { collapsed: { position: [-0.5, -0.8, -1], opacity: 0, }, expanded: { position: [-1, -1.25, 1], opacity: 1, }, }; Three.js: The meshes (objects present in your scene) export const Sphere = () => { const { isHovering } = useContext(ButtonContext); }; return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> </animated.mesh> ); const { position } = useSpring( isHovering ? states.expanded : states.collapsed );
  88. The state of Web APIs Output Reference const states =

    { collapsed: { position: [-0.5, -0.8, -1], opacity: 0, }, expanded: { position: [-1, -1.25, 1], opacity: 1, }, }; export const Sphere = () => { const { isHovering } = useContext(ButtonContext); const { position, opacity } = useSpring( isHovering ? states.expanded : states.collapsed ); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); }; Three.js: The materials (styles definitions for your meshes)
  89. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The lights (control how your materials act)
  90. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <ambientLight intensity={1} color='#D1D1D1' /> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The lights (control how your materials act)
  91. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <ambientLight intensity={1} color='#D1D1D1' /> <pointLight intensity={10} color='#FF0055' position={[300, -100, -100]} /> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The lights (control how your materials act)
  92. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <ambientLight intensity={1} color='#D1D1D1' /> <pointLight intensity={10} color='#FF0055' position={[300, -100, -100]} /> <pointLight intensity={10} color='#FF0055' position={[300, 100, -100]} /> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The lights (control how your materials act)
  93. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <ambientLight intensity={1} color='#D1D1D1' /> <pointLight intensity={10} color='#FF0055' position={[300, -100, -100]} /> <pointLight intensity={10} color='#FF0055' position={[300, 100, -100]} /> <pointLight intensity={20} color='#FF0055' position={[69, 49, -20]} /> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The lights (control how your materials act)
  94. The state of Web APIs Output Reference Three.js: The lights

    (control how your materials act) import { Canvas } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <ambientLight intensity={1} color='#D1D1D1' /> <pointLight intensity={10} color='#FF0055' position={[300, -100, -100]} /> <pointLight intensity={10} color='#FF0055' position={[300, 100, -100]} /> <pointLight intensity={20} color='#FF0055' position={[69, 49, -20]} /> <pointLight intensity={1} color='#fff' position={[900, -900, 2000]} /> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> );
  95. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <ambientLight intensity={1} color='#D1D1D1' /> <pointLight intensity={10} color='#FF0055' position={[300, -100, -100]} /> <pointLight intensity={10} color='#FF0055' position={[300, 100, -100]} /> <pointLight intensity={20} color='#FF0055' position={[69, 49, -20]} /> <pointLight intensity={1} color='#fff' position={[900, -900, 2000]} /> <pointLight intensity={20} color='#6ed5fa' position={[-750, 750, 500]} /> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The lights (control how your materials act)
  96. The state of Web APIs Output Reference import { Canvas

    } from ‘@react-three/ fi ber'; export const ButtonCanvas = () => ( <Canvas> <ambientLight intensity={1} color='#D1D1D1' /> <pointLight intensity={10} color='#FF0055' position={[300, -100, -100]} /> <pointLight intensity={10} color='#FF0055' position={[300, 100, -100]} /> <pointLight intensity={20} color='#FF0055' position={[69, 49, -20]} /> <pointLight intensity={1} color='#fff' position={[900, -900, 2000]} /> <pointLight intensity={20} color='#6ed5fa' position={[-750, 750, 500]} /> <Torus /> <Cone /> <Sphere /> <Icosahedron /> </Canvas> ); Three.js: The lights (control how your materials act)
  97. Will-change may be a pure evil Optimizing the animation .class

    { transition: <property> <duration> <timing-function> <delay>; will-change: <animateable-feature>; ⚠ }
  98. Will-change may be a pure evil Optimizing the animation .class

    { transition: <property> <duration> <timing-function> <delay>; will-change: <animateable-feature>; ⚠ } Creates composition layer
  99. Don’ts: Premature Optimization Optimizing the animation “The real problem is

    that programmers have spent far too much time worrying about ef fi ciency in the wrong places and at the wrong times; premature optimization is the root of all evil in programming.” Donald Knuth
  100. Framework-wise optimization Optimizing the animation export const Button = ({

    onClick }) => { const [isHovering, setIsHovering] = useState(false); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; return ( <button onClick={onClick}> <ButtonContext.Provider value={{ isHovering }}> ... <ButtonCanvas /> ... </ButtonContext.Provider> </button> ); };
  101. Framework-wise optimization Optimizing the animation const states = { ...

    }; export const Sphere = () => { const { isHovering } = useContext(ButtonContext); const { position, opacity } = useSpring(isHovering ? states.expanded : states.collapsed); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  102. Framework-wise optimization Optimizing the animation const states = { ...

    }; let rerenders = 0; export const Sphere = () => { console.log(rerenders++); const { isHovering } = useContext(ButtonContext); const { position, opacity } = useSpring(isHovering ? states.expanded : states.collapsed); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  103. Framework-wise optimization Optimizing the animation const states = { ...

    }; let rerenders = 0; export const Sphere = () => { console.log(rerenders++); const { isHovering } = useContext(ButtonContext); const { position, opacity } = useSpring(isHovering ? states.expanded : states.collapsed); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  104. Framework-wise optimization Optimizing the animation const states = { ...

    }; let rerenders = 0; export const Sphere = () => { console.log(rerenders++); const { isHovering } = useContext(ButtonContext); const { position, opacity } = useSpring(isHovering ? states.expanded : states.collapsed); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  105. Framework-wise optimization Optimizing the animation const states = { ...

    }; let rerenders = 0; export const Sphere = () => { console.log(rerenders++); const { isHovering } = useContext(ButtonContext); const springApi = useSpringRef(); const { position, opacity } = useSpring({ ref: springApi, from: states.collapsed, }); useEffect(() => { springApi.start({ to: isHovering ? states.expanded : states.collapsed }); }, [isHovering, springApi]); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  106. Framework-wise optimization Optimizing the animation const states = { ...

    }; let rerenders = 0; export const Sphere = () => { console.log(rerenders++); const { isHovering } = useContext(ButtonContext); const springApi = useSpringRef(); const { position, opacity } = useSpring({ ref: springApi, from: states.collapsed, }); useEffect(() => { springApi.start({ to: isHovering ? states.expanded : states.collapsed }); }, [isHovering, springApi]); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  107. Framework-wise optimization Optimizing the animation const states = { ...

    }; let rerenders = 0; export const Sphere = () => { console.log(rerenders++); const { isHovering } = useContext(ButtonContext); const springApi = useSpringRef(); const { position, opacity } = useSpring({ ref: springApi, from: states.collapsed, }); useEffect(() => { springApi.start({ to: isHovering ? states.expanded : states.collapsed }); }, [isHovering, springApi]); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  108. Framework-wise optimization Optimizing the animation const states = { ...

    }; let rerenders = 0; export const Sphere = () => { console.log(rerenders++); const { isHovering } = useContext(ButtonContext); const springApi = useSpringRef(); const { position, opacity } = useSpring({ ref: springApi, from: states.collapsed, }); useEffect(() => { springApi.start({ to: isHovering ? states.expanded : states.collapsed }); }, [isHovering, springApi]); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  109. Framework-wise optimization Optimizing the animation export const Sphere = ()

    => { console.log(rerenders++); const { subscribeHoverChange } = useContext(ButtonContext); const springApi = useSpringRef(); const { position, opacity } = useSpring({ ref: springApi, from: states.collapsed, }); useEffect(() => { const handleOnHover = (isHovering) => { springApi.start({ to: isHovering ? states.expanded : states.collapsed }); }; subscribeHoverChange(handleOnHover); }, [subscribeHoverChange, springApi]); return ( <animated.mesh position={position}> <sphereGeometry attach='geometry' args={[1, 32, 32]} /> <GeometryMaterial opacity={opacity} /> </animated.mesh> ); };
  110. Framework-wise optimization Optimizing the animation export const Button = ({

    onClick }) => { const [isHovering, setIsHovering] = useState(false); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; return ( <button onClick={onClick}> <ButtonContext.Provider value={{ isHovering }}> ... <ButtonCanvas /> ... </ButtonContext.Provider> </button> ); };
  111. Framework-wise optimization Optimizing the animation export const Button = ({

    onClick }) => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = (callback) => { subscribers.current.push(callback); }; return ( <button onClick={onClick}> <ButtonContext.Provider value={{ subscribeHoverChange }}> ... <ButtonCanvas /> ... </ButtonContext.Provider> </button> ); };
  112. Optimizing the animation export const Button = ({ onClick })

    => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = (callback) => { subscribers.current.push(callback); }; return ( <button onClick={onClick}> <ButtonContext.Provider value={{ subscribeHoverChange }}> ... <ButtonCanvas /> ... </ButtonContext.Provider> </button> ); }; Framework-wise optimization
  113. Framework-wise optimization Optimizing the animation export const Button = ({

    onClick }) => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); useEffect(() => notifySubscribers(), [notifySubscribers]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = (callback) => { subscribers.current.push(callback); }; return ( <button onClick={onClick}> <ButtonContext.Provider value={{ subscribeHoverChange }}> ... <ButtonCanvas /> ... </ButtonContext.Provider> </button> );
  114. Optimizing the animation export const Button = ({ onClick })

    => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); useEffect(() => notifySubscribers(), [notifySubscribers]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = (callback) => { subscribers.current.push(callback); }; return ( <button onClick={onClick}> <ButtonContext.Provider value={{ subscribeHoverChange }}> ... <ButtonCanvas /> ... </ButtonContext.Provider> </button> ); Framework-wise optimization
  115. Optimizing the animation export const Button = ({ onClick })

    => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); useEffect(() => notifySubscribers(), [notifySubscribers]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = useCallback((callback) => { subscribers.current.push(callback); }, []); return ( <button onClick={onClick}> <ButtonContext.Provider value={{ subscribeHoverChange }}> ... <ButtonCanvas /> ... </ButtonContext.Provider> </button> ); Framework-wise optimization
  116. Optimizing the animation const MemoizedButtonCanvas = memo(ButtonCanvas); export const Button

    = ({ onClick }) => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); useEffect(() => notifySubscribers(), [notifySubscribers]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = useCallback((callback) => { subscribers.current.push(callback); }, []); return ( <button onClick={onClick}> <ButtonContext.Provider value={{ subscribeHoverChange }}> ... <MemoizedButtonCanvas containerClassName='button__canvas' /> ... </ButtonContext.Provider> </button> ); }; Framework-wise optimization
  117. Optimizing the animation const MemoizedButtonCanvas = memo(ButtonCanvas); export const Button

    = ({ onClick }) => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); useEffect(() => notifySubscribers(), [notifySubscribers]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = useCallback((callback) => { subscribers.current.push(callback); }, []); return ( <button onClick={onClick}> <ButtonContext.Provider value={{ subscribeHoverChange }}> ... <MemoizedButtonCanvas containerClassName='button__canvas' /> ... </ButtonContext.Provider> </button> ); }; Framework-wise optimization
  118. Optimizing the animation const MemoizedButtonCanvas = memo(ButtonCanvas); export const Button

    = ({ onClick }) => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); useEffect(() => notifySubscribers(), [notifySubscribers]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = useCallback((callback) => { subscribers.current.push(callback); }, []); const props = useMemo(() => ({ subscribeHoverChange }), [subscribeHoverChange]); return ( <button onClick={onClick}> <ButtonContext.Provider value={props}> ... <MemoizedButtonCanvas containerClassName='button__canvas' /> ... </ButtonContext.Provider> </button> ); Framework-wise optimization
  119. Optimizing the animation const MemoizedButtonCanvas = memo(ButtonCanvas); export const Button

    = ({ onClick }) => { const [isHovering, setIsHovering] = useState(false); const subscribers = useRef([]); const notifySubscribers = useCallback( () => subscribers.current.forEach((fn) => fn(isHovering)), [isHovering] ); useEffect(() => notifySubscribers(), [notifySubscribers]); const { transform } = useSpring({ transform: isHovering ? 'scale(1.0)' : 'scale(0.8)' }); const handleMouseEnter = (...) => { ...; setIsHovering(true); }; const handleMouseLeave = (...) => { ...; setIsHovering(false); }; const subscribeHoverChange = useCallback((callback) => { subscribers.current.push(callback); }, []); const props = useMemo(() => ({ subscribeHoverChange }), [subscribeHoverChange]); return ( <button onClick={onClick}> <ButtonContext.Provider value={props}> ... <MemoizedButtonCanvas containerClassName='button__canvas' /> ... </ButtonContext.Provider> </button> ); Framework-wise optimization
  120. Should I even bother with animations? The state of Web

    APIs Optimizing the animation Accessible animations
  121. Let users decide Accessible animations All motions enabled? Perfect. Only

    gentle motions? No problem. All motions disabled? Sure. Give users more control over your product
  122. Prefers reduced motion Accessible animations @media (prefers-reduced-motion) { /* styles

    to apply if the user's settings are set to reduced motion */ } Has user requested the system to disable non-essential
  123. Prefers reduced motion Accessible animations @media (prefers-reduced-motion) { /* styles

    to apply if the user's settings are set to reduced motion */ } Has user requested the system to disable non-essential
  124. GIF? I want to see the frame. Accessible animations <picture>

    <source srcset="static.jpg" media="(prefers-reduced-motion: reduce)" /> <img src="dynamic.gif" alt="Nyan Cat" height="220" width="220" /> </picture> Replace GIF with static image on prefers-reduced-motion
  125. Accessible animations <picture> <source srcset="static.jpg" media="(prefers-reduced-motion: reduce)" /> <img src="dynamic.gif"

    alt="Nyan Cat" height="220" width="220" /> </picture> Replace GIF with static image on prefers-reduced-motion
  126. Don’ts: Overwriting native behaviors Accessible animations - Keep in mind

    platform-specific guidelines - Keep in mind Accessibility (eg. screen readers) - Don’t overwrite scroll!
  127. Frontend Engineer @ Upside • Google Code-In Winner • ACS

    @ UJ kawka.me • github.com/letelete • linkedin.com/in/brunokawka Thank you! Bruno Kawka