<onWebFocus />

Knowledge is only real when shared.

Animations on the Web

October 1, 2023

Looking for a consistent way to apply animations in React.

While animations are often employed as decorative elements on websites, they possess the remarkable ability to significantly enhance the overall user experience. However, implementing animations can be a nuanced process, with each approach carrying its unique set of constraints. As animations grow in complexity, the interfaces designed to control them can quickly become intricate.

But what exactly constitutes an animation? From a technical standpoint, an animation involves the gradual transformation of a property over a specified period, typically guided by a predefined function. These animation functions, often built into web development frameworks, rely on concepts such as interpolation to achieve a lifelike and fluid visual effect. Animations are commonly triggered in response to specific events or user interactions. Thanks to the advancements in browser capabilities, modern web browsers can efficiently handle animations for multiple elements simultaneously, enabling the creation of captivating and seamless scrolling experiences.

CSS Transitions

One of the simplest methods to introduce animations on a web page is by modifying a property within a CSS selector and applying a transition to it. By leveraging the third argument of the transition property or the transition-timing-function property, you gain the flexibility to tailor the mathematical function responsible for calculating the intermediate values between the initial and final states.

.link {
  opacity: 0.5;
  transition: opacity 1s ease-in;
}

.link:hover, link:active {
  opacity: 1;
}
Example
Hover for transition

In addition to controlling animations using pseudo-selectors like :hover, transitions on properties are frequently activated by adding an extra class, often referred to as a modifier, to the existing set of properties applied to an element.

CSS Keyframes

While slightly more verbose, keyframes offer the advantage of enabling intricate and multifaceted animations, extending their applicability to a wider range of properties. Keyframes are defined individually using the @keyframes prefix and can be subsequently assigned to any element through the animation property. This grants precise control over the animation's behavior, allowing for fine-tuned customization.

@keyframes enlarge {
  from {
    transform: scale(1);
  }

  to {
    transform: scale(1.5);
  }
}

.enlarge {
  background: blue;
  user-select: none;
}

.enlarge:hover {
  animation: 1s ease-in enlarge forwards;
}
Example
enlarge
rainbow

requestAnimationFrame

Beyond the built-in methods for animating properties such as transitions or keyframes, there's an option to implement animations manually. This approach expands the range of what can be animated, although it may come with a modest performance trade-off. Much like the more recent introduction of requestIdleCallback, a registered callback is executed as soon as the browser finishes rendering and the UI thread is idle. This callback is a one-time event and, unless the animation has completed, another callback is typically registered within the current one.

const element = document.getElementById('stretch')
let start, previousTimeStamp
let done = false

function step(timeStamp) {
  if (start === undefined) { start = timeStamp }
  const elapsed = timeStamp - start

  if (previousTimeStamp !== timeStamp) {
    const count = Math.min(0.1 * elapsed, 200) // Stop at 200px.
    // Regularly set the next property value on the element.
    element.style.transform = `translateX(${count}px)`
    if (count === 200) { done = true }
  }

  // Stop the animation after 2 seconds
  if (elapsed < 2000) {
    previousTimeStamp = timeStamp
    if (!done) {
      window.requestAnimationFrame(step)
    }
  }
}

window.requestAnimationFrame(step)
Example

From a performance perspective, this method is notably efficient, and most modern browsers running on decent hardware can maintain a smooth rendering pace of 60 frames per second. Furthermore, the performance generally matches that of using native methods. While overt signs like fans spinning loudly or noticeable lag suggest significant performance issues, it's also possible to discern more subtle concerns and make meaningful comparisons by leveraging the Frame Rendering Stats available in Chrome Dev Tools.

Web Animations API

The Web Animations API, supported across all major browsers, brings the fundamental CSS concepts of transitions and keyframes into the realm of JavaScript. Instead of relying on CSS selectors to apply animations to elements, the Web Animations API empowers developers to directly add animations to document nodes using JavaScript.

const element = document.getElementById('pulse')
const keyframes = [{ opacity: 1 }, { opacity: 0 }, { opacity: 1 }]
const options = { duration: 2000, iterations: Infinity }

element.animate(keyframes, options)
Example
Let me pulse

Thanks to its programmatic nature in JavaScript, animations created with this API can be easily modified or paused at a later time. Additionally, within an SVG context, this interface opens the door to SVG-specific animations, including effects such as movement along a defined path.

Animating SVG

SVG (Scalable Vector Graphics) provides a powerful means to embed and manipulate XML-based vector graphics directly within JSX. Unlike formats designed for easy interpretation, SVG excels when crafted in WYSIWYG (What You See Is What You Get) design tools such as Figma, allowing for seamless export as SVG files. React simplifies SVG rendering, enabling the creation of reusable components with customizable props. Animations can be seamlessly incorporated either within the code itself or by utilizing specialized design tools that introduce a temporal dimension, akin to the workflow in video editing software, for refining animations.

Canvas API

Canvas elements, available in virtually every web browser, offer a notably more efficient means of rendering 2D shapes compared to standard HTML and CSS. When it comes to animations within a canvas, the approach differs from traditional web animations. Much like the mechanics of game engines, the canvas is cleared and redrawn entirely with each frame. This approach allows for dynamic changes in the positions of elements between frames, resulting in fluid animations—similar to how the browser handles transitions and keyframes in the background.

const canvas = document.getElementById('canvas')

function draw() {
  const ctx = canvas.getContext('2d')

  // Clear the entire canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // Set up the blue background
  ctx.fillStyle = 'blue'
  ctx.fillRect(0, 0, 50, 50)

  // Calculate the center of the canvas
  const centerX = canvas.width / 2
  const centerY = canvas.height / 2
  const time = new Date()

  // Rotate position based on the current time
  ctx.translate(centerX, centerY)
  const angle =
    ((2 * Math.PI) / 6) * time.getSeconds() + ((2 * Math.PI) / 6000) * time.getMilliseconds()
  ctx.rotate(angle)
  ctx.translate(-centerX, -centerY)

  // Draw a white dot
  ctx.beginPath()
  ctx.arc(25, 5, 5, 0, Math.PI * 2)
  ctx.fillStyle = 'white'
  ctx.fill()
  ctx.closePath()

  // Restore rotation to its default
  ctx.setTransform(1, 0, 0, 1, 0, 0)

  // Request the next animation frame
  window.requestAnimationFrame(draw)
}

// Start the animation
window.requestAnimationFrame(draw)
Example
Basic rotation

Animation Inputs and Triggers

While continuous animations can sometimes overwhelm and distract users from the primary content of a web page, animations are typically event-driven, adding a layer of interactivity and engagement. A common scenario is animations triggered upon entry, occurring once immediately after elements have been initially rendered. This approach assumes that elements are initially positioned correctly, which means they may not be immediately visible if their appearance is animated. A variation of this approach involvesin-view events, ensuring animations activate only when the user can actually see the element on the screen. On desktop interfaces, hover animations are prevalent, signaling interactivity when the user's cursor hovers over an element. Likewise, the focus event activates when a user interacts with an element, and clicking interactive elements often triggers animations. Animations tied to scroll positions are widely employed in marketing websites, while cursor position-based animations respond to the mouse's movements.

With the exception of the initial rendering event, most animations require event registration. Some listeners, like scroll and cursor events, can be attached once to the document and used to trigger various animations based on specific conditions. However, most other listeners must be added to individual elements after rendering. React is well-equipped to handle some of these events, such as the onClick callback. However, for animation-specific events, developers may need to construct custom solutions by combiningonFocus and onBlur to replicate the functionality of a :hover CSS pseudo-selector. While absent from React animation frameworks, the :active selector can be emulated in JavaScript by capturing the mousedown and mouseup events. It's important to note that for privacy reasons, access to the :visited selector is restricted in JavaScript, and it's advisable to avoid its use altogether.

On mobile devices, separate events for touch and gestures are available, further enhancing the range of interactive possibilities while adapting to the unique characteristics of touch interfaces. Some of the triggers mentioned earlier may not be applicable in mobile contexts.

Example
Entry
In-View
Hover
Active
Focus
Click
Scroll
Cursor

Animations in React

While there isn't a one-size-fits-all approach to animating elements in React, several popular methods have emerged. Traditional transitions and keyframes defined in CSS and applied to React elements through className continue to be widely used, especially in React applications that incorporate CSS. Many CSS-in-JS frameworks, such as styled-components and@stitches/react, offer the capability to inject keyframes into CSS through JavaScript, seamlessly combining them with regular transitions applied to styled components.

In the realm of React Native, a different approach emerges, where animated values take center stage. These values operate similarly to state but change much faster and don't necessitate a component rerender upon modification. Wrapper components like Animated.View in React Native allow for the direct application of animated values to native nodes, providing a performant means of achieving dynamic animations.

Frameworks for React

For those seeking more structured animation solutions, the react-transition-group plugin has stood the test of time. It provides Higher-Order Components (HOCs) for various types of transitions, making it a popular choice for managing animations in React applications.

framer-motion stands out as one of the most widely embraced frameworks for web-based animations. It introduces custom elements that can be effortlessly animated by incorporating additional properties. Furthermore, Framer Motion provides hooks that return animatable values, functioning much like state but capable of dynamic changes without necessitating a component rerender. This versatility makes it a go-to choice for creating fluid and engaging user experiences.

import { motion } from "framer-motion"

const div = ({ isVisible }) => <motion.div animate={{ opacity: isVisible ? 1 : 0 }} />

From the same developer as Framer Motion, the motion library offers a leaner, framework-agnostic approach to animations. It relies on the Web Animations API for support and, while minimalistic, boasts exceptional performance. Motion allows animations to be directly applied to HTML elements, simplifying the process of bringing motion to your web applications.

In the realm of React Native, react-native-reanimated reigns as a popular animation utility. It shares similarities with the built-in Animated.View but brings enhanced functionality to mobile development. Like its web counterpart, Framer Motion, react-native-reanimated employs a similar approach to animations. However, it distinguishes between the JavaScript and UI threads, optimizing performance and responsiveness in mobile applications.

For those seeking a library that streamlines the implementation of keyframe animations,animate.css is a valuable resource. It offers a collection of CSS classes that can be effortlessly added to elements, instantly animating them. To facilitate the discovery and understanding of these animations, animate.css provides visual previews of each animation on animate.style, making it easier to choose and apply the perfect animation effect.

Emerging from the React Three Fiber ecosystem, react-spring focuses on value interpolation. This framework provides animatable components, values, and hooks, mirroring the approach of Framer Motion.

For React Native developers, react-native-skia introduces the power of the 2D Skiagraphics library. In a manner akin to SVG, forms and shapes are rendered using JSX. This approach is especially valuable in React Native, where SVG support is limited. By leveraging native dependencies, react-native-skia delivers high-performance rendering of intricate graphics. While usable on the web, it's worth noting that running the Skia engine in JavaScript via a regular canvas can significantly increase bundle size. The framework employs a similar animation approach to react-native-reanimated, offering its own animation hooks for precise control over property values over time.

Conclusion

While CSS-in-JS allows users to create customized styled components external to the React component, React-specific animation frameworks take a different approach. They leverage fundamental React features such as JSX and hooks, integrating animation logic directly into the rendering process. However, this integration presents certain challenges. Animations, driven by the need for optimal performance, must often occur without waiting for a component to fully rerender between value changes. Consequently, the marriage of animation and UI rendering within the React framework can be somewhat less than ideal. Moreover, different types of animations may require significantly different interfaces, further complicating the animation development landscape.

Toward a Consistent Animation Interface

While the web offers various techniques for animating elements using CSS, JavaScript, and React, there has yet to be a universally compelling approach. This often leaves developers grappling with a multitude of methods, requiring them to learn and adapt to different approaches for various animation scenarios. The quest is to discover an effective approach that allows developers to animate JSX-rendered elements consistently and seamlessly, regardless of the animation type.

When embarking on the design of such a library, it's crucial to consider the context in which animations operate. Animations don't necessarily require access to the component rendering cycle, despite current approaches often involving hooks or higher-order components directly within JSX. Instead, animations come into play once elements have been committed to the UI. At this stage, they rely on three key components: the native elements or refs, animation triggers like hover, scroll, or click events, and the application's state—both global and component-specific.

To meet these requirements, a more effective approach involves decoupling the component state from the JSX and granting the state direct access to the elements and their associated lifecycle events. Animation triggers and global state can be imported and utilized as needed, creating a cleaner and more modular architecture.

import { hover, click, animate } from 'tbd'

class ComponentState {
  valid = false
  name = ''

  mount() {
    animate(this.input, 'appear')
    animate(this.button, 'appear')
  }

  setName(name: string) {
    this.name = name
  }

  @hover('button')
  highlightButton(button: HTMLButtonElement) {
    animate(button, { opacity: 1 })
    return animate(button, { opacity: 0.8 })
  }

  @click('button')
  submit() {
    if (!this.valid) {
      animate(this.input, { borderColor: 'red' })
    } else {
      this.form.submit()
    }
  }
}

export function Form<Props, ComponentState>(props, state) {
  return (
    <form id="form">
      <input id="input" value={state.name} onChange={state.setName} />
      <button id="button" style={{ opacity: 0.8 }} disabled={!state.valid} type="submit">
        Submit
      </button>
    </form>
  )
}

Form.state = ComponentState

The pseudo-code above provides a preliminary glimpse of how this separation of animation logic into component state could be implemented. Animation triggers are seamlessly incorporated into state classes using ES Decorators, a relatively underutilized feature in React but well-supported, especially in conjunction with TypeScript.

This post was revised with ChatGPT a Large Language Model.