Home / Blog / HTML & CSS

HTML & CSS

Crossfade CSS Linear Gradients Without Moving Text

Create a CSS linear-gradient crossfade with two background layers, a pause checkbox, and reduced-motion support. Keep text separate from the fading layer.

Animate the opacity of a second gradient

A CSS linear-gradient animation can fade between two complete palettes while leaving the layout still. Put the first gradient on a container, put the second on a pseudo-element, and animate that pseudo-element’s opacity. The browser blends the two painted layers. You do not need to assume that changing background-image itself will interpolate smoothly.

This method suits a decorative panel where the angle or color arrangement should change gently. It differs from sliding a large background: each gradient continues to fill the whole panel, and the fade mixes their contributions. At the midpoint, the visible result combines the two surfaces rather than displaying a third gradient that you explicitly authored.

Keep the fading surface behind the content

The inner content wrapper is important. It sits above the decorative pseudo-element, so neither text nor links fade away. The outer demo contains a checkbox that controls the animation. The pseudo-element cannot capture pointer events, which prevents the decorative layer from blocking a real link placed in the content wrapper.

<div class="fade-demo">
  <input id="pause-fade" class="fade-toggle" type="checkbox">
  <label for="pause-fade">Pause color animation</label>
  <section class="fade-panel">
    <div class="fade-content">
      <h2>Two palettes, one panel</h2>
      <p>Only the decorative layer fades.</p>
    </div>
  </section>
</div>
.fade-demo { max-width: 48rem; margin: 1rem auto; }
.fade-toggle:focus-visible { outline: 3px solid #111827; }
.fade-panel {
  position: relative;
  isolation: isolate;
  margin-top: .75rem;
  overflow: hidden;
  border-radius: 1rem;
  color: white;
  background: linear-gradient(120deg, #172554, #4c1d95);
}
.fade-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(240deg, #134e4a, #1e3a8a);
  opacity: 0;
  animation: palette-fade 6s ease-in-out infinite alternate;
}
.fade-content { position: relative; z-index: 1; padding: 2rem; }
@keyframes palette-fade { to { opacity: 1; } }
.fade-toggle:checked ~ .fade-panel::before {
  animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
  .fade-panel::before { animation: none; opacity: 0; }
}

The pseudo-element starts transparent and reaches full opacity after six seconds. Alternate then fades it back. The panel’s base gradient remains painted underneath throughout the cycle, so there is no transparent gap. The rounded clipping belongs to the panel, making both backgrounds follow the same corners at every opacity.

Generate and inspect the two static endpoints

Use the CSS Gradient Generator twice. First choose Linear, angle 120, #172554 and #4c1d95. Then choose angle 240, #134e4a and #1e3a8a. Put the first result on .fade-panel and the second on its ::before rule. Each static preview should look acceptable before adding the fade.

The generator does not export layered markup or an animation. It produces one two-color background declaration at a time. The wrapper, pseudo-element and keyframes in this guide provide the composition. If you need more color stops, edit the CSS manually and test them as part of each full endpoint rather than expecting an extra stop control in the tool.

Test the endpoints, midpoint and controls

Pause the animation at its starting color, halfway through the outward fade, and at the second palette. In developer tools, inspect ::before and confirm that opacity moves from zero through a fractional value to one. The content wrapper’s own opacity should remain one in every state. That distinction catches the common mistake of animating the whole panel.

Use the keyboard to reach the checkbox, press Space, and watch the pseudo-element stop. Verify that any links or buttons inside .fade-content remain operable. At a narrow width, text should wrap naturally; the absolutely positioned background takes its size from the panel and does not require a fixed width or height.

Choose a readable static fallback

With reduced motion enabled, this recipe removes the animation and hides the second layer, leaving the first palette. That first view therefore needs to work on its own. Changing it to a low-contrast experimental color and relying on a later animation frame to become readable would also break the static experience.

Opacity animation can often be handled efficiently, but a large animated surface still deserves testing on the devices you support. Avoid adding permanent will-change declarations to every card without measuring a benefit. A page containing many independent fades can consume more resources and attention than a single decorative header.

If the text fades too, move the animation rule back to ::before. If the overlay vanishes completely, inspect its generated content, absolute positioning and computed opacity before changing the palette. Those layout properties must work before color selection can improve the result.

The linear-gradient reference describes the static image values used at each endpoint. For the animation itself, the CSS Animations specification explains keyframes and alternate iteration. This recipe combines those mechanisms without making a promise about direct interpolation between arbitrary gradient images.