Recreate jQuery fadeIn with CSS and Vanilla JS

Transitioning away from jQuery often requires finding native alternatives to built-in animation methods like .fadeIn(). This guide demonstrates how to replicate jQuery's .fadeIn() functionality using modern vanilla JavaScript and CSS transitions. By combining native DOM manipulation with hardware-accelerated CSS properties, you can achieve smoother, more performant fade-in animations without relying on external dependencies.

The Challenge with Display and Opacity

In jQuery, .fadeIn() handles both setting the element's CSS display property and animating its opacity. In standard CSS, the display property cannot be animated. If you change an element from display: none to display: block and simultaneously alter its opacity, the browser executes both at once, resulting in an abrupt appearance rather than a smooth fade.

To solve this natively, you must reveal the element in the document flow, trigger a browser reflow or defer the opacity change by a frame, and then transition the opacity to 1.


1. The CSS Setup

Define the base transition styles and utility classes. The base element starts hidden with an opacity of 0, and transitioning the opacity handles the fade effect.

.fade-element {
  opacity: 0;
  display: none;
  transition: opacity 400ms ease-in-out;
}

.fade-element.is-visible {
  opacity: 1;
}

2. The Vanilla JavaScript Implementation

To make the transition work, switch display to a visible value (such as block or inline-block), force the browser to compute the updated styles, and then apply the class that increases the opacity.

function fadeIn(element, displayType = 'block') {
  // 1. Make the element part of the layout
  element.style.display = displayType;

  // 2. Force a reflow to ensure the browser registers the initial opacity of 0
  void element.offsetWidth;

  // 3. Trigger the transition by adding the active class
  element.classList.add('is-visible');
}

Reading element.offsetWidth forces the browser to recalculate styles (a reflow), guaranteeing that the transition from 0 to 1 takes place smoothly instead of instantly jumping to the final state.


3. Creating a Dynamic, Standalone Function

If you need a flexible function that accepts custom durations—mimicking the duration argument in jQuery's .fadeIn(duration)—you can control the inline styles entirely via JavaScript:

function fadeIn(element, duration = 400, displayType = 'block') {
  element.style.opacity = '0';
  element.style.display = displayType;
  element.style.transition = `opacity ${duration}ms ease-in-out`;

  // Use requestAnimationFrame to apply the opacity change on the next paint
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      element.style.opacity = '1';
    });
  });
}

Complete Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Native fadeIn Example</title>
  <style>
    #box {
      width: 200px;
      height: 200px;
      background-color: #007acc;
      display: none;
      opacity: 0;
      transition: opacity 500ms ease;
    }

    #box.visible {
      opacity: 1;
    }
  </style>
</head>
<body>

  <button id="trigger">Fade In</button>
  <div id="box"></div>

  <script>
    const button = document.getElementById('trigger');
    const box = document.getElementById('box');

    function fadeIn(el) {
      el.style.display = 'block';
      void el.offsetWidth; // Trigger reflow
      el.classList.add('visible');
    }

    button.addEventListener('click', () => {
      fadeIn(box);
    });
  </script>

</body>
</html>

This pattern provides the same visual result as jQuery's .fadeIn() method while running natively on the browser's compositor thread for optimal rendering performance.