How to Implement Blur Transitions to Mask Imperfect Crossfades
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 at line 292, which explicitly recommends "blur to mask crossfades" as a standard practice. The complementary rule in 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.mdline 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
filterproperties 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.mdline 73) - Accessibility: Respect
prefers-reduced-transparencyby 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.
.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 */
}
<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
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>
);
}
/* 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:
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 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-changesparingly and remove after animation completes
Reduced Transparency Support
Following skills/apple-design/SKILL.md line 204, implement accessibility fallbacks:
.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 - Apply blur to both entering and exiting elements for the full duration of the opacity transition
- Animate
filteralongsideopacitywith identical timing functions to prevent visual tearing - Respect 20px maximum blur radius to maintain Safari performance per
skills/improve-animations/AUDIT.md - Always provide
prefers-reduced-transparencyandprefers-reduced-motionfallbacks 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.
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 recommends profiling with browser DevTools and keeping blur radius under 20px maximum.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →