# How to Implement Blur Transitions to Mask Imperfect Crossfades

> Implement blur transitions with a 2px CSS filter to mask imperfect crossfades. Soften edges and eliminate double vision glitches for smooth UI state changes.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: how-to-guide
- Published: 2026-08-05

---

**Apply a 2px CSS blur filter during crossfade animations to soften overlapping edges and eliminate the "double-vision" glitch that occurs when two UI states render simultaneously.**

Crossfade transitions between UI states often suffer from visual imperfection: for a brief moment, both the outgoing and incoming elements are partially visible, creating a distracting overlap. The `emilkowalski/skills` repository documents a proven solution—adding controlled blur to mask these imperfections. This technique appears in multiple motion design standards files and has become a foundational pattern for polished web animations.

## Why Blur Fixes Crossfade Glitches

When two elements crossfade without blur, human eyes perceive the overlapping period as problematic double vision. A small amount of blur (`filter: blur(2px)`) softens the edges of both layers, causing them to visually merge into a single fluid transformation rather than two competing images.

The core rationale appears in **[`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)** at line 292, which explicitly recommends "blur to mask crossfades" as a standard practice. The complementary rule in **[`skills/review-animations/STANDARDS.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md)** at line 147 reinforces this guidance with concrete implementation details.

## Core Implementation Guidelines

The repository establishes specific constraints for effective blur masking:

- **Blur Amount**: Keep radius modest—**2px is the standard value**, with a hard ceiling of 20px to prevent performance degradation (per **[`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)** line 317)
- **Timing**: Blur must span the *entire* crossfade duration, starting when opacity begins changing and ending when the incoming element reaches full opacity
- **Performance**: Prefer CSS `filter` properties over SVG or Canvas filters; they leverage GPU acceleration
- **Combination**: Pair with additional polish techniques like subtle scale-down on press for richer feel (see **[`skills/review-animations/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/SKILL.md)** line 73)
- **Accessibility**: Respect `prefers-reduced-transparency` by disabling blur for users who need reduced motion

## CSS-Only Blur Crossfade Implementation

The most framework-agnostic approach uses CSS transitions on opacity and filter simultaneously.

```css
.fade-wrapper {
  position: relative;
  perspective: 1000px;  /* Optional: adds subtle depth perception */
}

/* Base state for crossfading elements */
.fade-item {
  position: absolute;
  inset: 0;
  opacity: 0;
  filter: blur(0);
  transition:
    opacity 250ms ease,
    filter 250ms ease;  /* Synchronized durations prevent visual tearing */
  will-change: opacity, filter;  /* GPU optimization hint */
}

/* Visible state with blur applied during transition */
.fade-item.is-visible {
  opacity: 1;
  filter: blur(2px);  /* Softens edges to mask overlap */
}

```

```html
<div class="fade-wrapper">
  <div class="fade-item is-visible" id="state-a">Content A</div>
  <div class="fade-item" id="state-b">Content B</div>
</div>

```

Toggling `is-visible` between elements triggers the synchronized opacity and blur transition. The 2px blur applies to *both* elements during the overlap period, creating the masking effect.

## React Implementation Patterns

### React Transition Group Approach

```tsx
import { TransitionGroup, CSSTransition } from 'react-transition-group';

function BlurCrossfade({ children, activeKey }) {
  return (
    <TransitionGroup component={null}>
      <CSSTransition
        key={activeKey}
        timeout={250}
        classNames="fade"
      >
        <div className="fade-item">{children}</div>
      </CSSTransition>
    </TransitionGroup>
  );
}

```

```css
/* Transition group class-based animation */
.fade-enter {
  opacity: 0;
  filter: blur(2px);
}

.fade-enter-active {
  opacity: 1;
  filter: blur(2px);
  transition: opacity 250ms ease, filter 250ms ease;
}

.fade-exit {
  opacity: 1;
  filter: blur(2px);
}

.fade-exit-active {
  opacity: 0;
  filter: blur(2px);
  transition: opacity 250ms ease, filter 250ms ease;
}

```

### Framer Motion Implementation

For declarative animation control:

```tsx
import { motion, AnimatePresence } from 'framer-motion';

function AnimatedView({ view, content }) {
  return (
    <AnimatePresence mode="sync">
      <motion.div
        key={view}
        initial={{ opacity: 0, filter: 'blur(2px)' }}
        animate={{ opacity: 1, filter: 'blur(2px)' }}
        exit={{ opacity: 0, filter: 'blur(2px)' }}
        transition={{ duration: 0.25, ease: 'easeOut' }}
      >
        {content}
      </motion.div>
    </AnimatePresence>
  );
}

```

Note: Framer Motion applies blur to entering, active, and exiting states to maintain the masking effect throughout the full crossfade duration.

## Performance and Accessibility Considerations

### Safari Performance Limits

The **[`skills/improve-animations/AUDIT.md`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md)** file at line 78 specifically warns that large blur radii cause dropped frames in Safari. The repository mandates:

- Never exceed 20px blur radius
- Test on actual iOS devices, not just simulators
- Use `will-change` sparingly and remove after animation completes

### Reduced Transparency Support

Following **[`skills/apple-design/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/apple-design/SKILL.md)** line 204, implement accessibility fallbacks:

```css
.fade-item {
  transition: opacity 250ms ease, filter 250ms ease;
}

@media (prefers-reduced-transparency: reduce) {
  .fade-item,
  .fade-item.is-visible {
    filter: none !important;  /* Disable blur for accessibility */
  }
}

/* Broader reduced-motion fallback */
@media (prefers-reduced-motion: reduce) {
  .fade-item {
    transition: opacity 50ms linear;  /* Near-instant, no blur */
    filter: none !important;
  }
}

```

## Summary

- **2px blur radius** is the standard value for masking crossfade imperfections, as documented in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)
- Apply blur to **both entering and exiting elements** for the **full duration** of the opacity transition
- Animate `filter` alongside `opacity` with **identical timing functions** to prevent visual tearing
- Respect **20px maximum blur radius** to maintain Safari performance per [`skills/improve-animations/AUDIT.md`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md)
- Always provide **`prefers-reduced-transparency`** and **`prefers-reduced-motion`** fallbacks for accessibility

## Frequently Asked Questions

### Why does crossfade without blur look broken?

Human visual perception registers overlapping semi-transparent elements as "double vision"—two distinct images competing for attention. A small blur softens the hard edges of both layers, causing the eye to interpret them as a single transforming element rather than two separate objects. The `emilkowalski/skills` repository identifies this as the primary motivation for blur masking in [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md).

### Should blur apply to entering elements, exiting elements, or both?

Both. The blur must cover the entire period where any overlap exists. When the outgoing element fades from 100% to 0% opacity and the incoming element fades from 0% to 100%, both are partially visible simultaneously. Applying 2px blur to both ensures consistent edge softening throughout the transition.

### How do I implement this without a JavaScript animation library?

Use pure CSS with absolute positioning and class toggling. Place both elements in a container with `position: relative`, set each element to `position: absolute; inset: 0`, and toggle an `is-visible` class that changes `opacity` and `filter: blur(2px)` simultaneously. The CSS transition handles the interpolation automatically.

### Does blur impact Core Web Vitals or page performance?

Minimal blur (2px) has negligible impact when applied to small elements. However, animating large blur radii or applying blur to large surface areas can cause layout shifts and dropped frames, particularly in Safari. The repository's [`skills/improve-animations/AUDIT.md`](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md) recommends profiling with browser DevTools and keeping blur radius under 20px maximum.