{product.name}
\n{product.description}
\n ${product.price}\nThis content is server-rendered and visible to search engines
\n \nThis content is ALSO server-rendered
\nAll this content is immediately visible to search engines
\nLightning fast performance
\nBank-level encryption
\n{product.description}
\n ${product.price}\n{{ product.description }}
\n {{ product.price }}\n{description}
\nWe provide world-class solutions
\n

Animations can transform a static website into an engaging, memorable experience. But when you're working with server-side rendering (SSR), adding animations introduces unique challenges that can trip up even experienced developers.
If you've ever encountered flickering animations, hydration mismatches, or performance issues when deploying animated SSR sites, you're not alone. The good news? There are proven techniques to integrate smooth, performant animations that work seamlessly with both Angular Universal and Next.js.
In this guide, we'll walk through five powerful techniques for implementing animations in SSR applications, with dedicated sections for Angular and React (Next.js) developers.
Before diving into solutions, let's understand the challenge. Server-side rendering generates HTML on the server, which means:
The SEO challenge is critical: When you mark a component with 'use client' or disable SSR, search engine crawlers might not see your content in the initial HTML. This defeats the entire purpose of using SSR in the first place.
The key is choosing techniques that gracefully handle the server-to-client handoff while maintaining smooth performance AND keeping your content visible to search engines.
Why it works: CSS animations run independently of JavaScript and can be included in your initial server-rendered HTML.
Implementation approach:
Best for: Loading animations, hover effects, simple transitions, and any animation that doesn't require complex logic.
Pro tip: Use the prefers-reduced-motion media query to respect user accessibility preferences:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Why it works: By loading animation libraries only on the client side, you avoid SSR conflicts and reduce initial bundle size.
Implementation approach:
Best for: Complex scroll-based animations, timeline-based sequences, and interactive animations that require DOM measurements.
Performance benefit: This technique can reduce your initial JavaScript bundle by 50-100KB or more, significantly improving Time to Interactive (TTI).
Why it works: The Intersection Observer API is browser-native, performant, and works perfectly with SSR when properly implemented.
Implementation approach:
Best for: Fade-in effects, slide-in animations, staggered lists, and any animation triggered by scrolling.
Code pattern:
// Pseudo-code pattern
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
}
});
});
elements.forEach(el => observer.observe(el));
return () => observer.disconnect();
}, []);
Why it works: By managing animation states in your component logic, you have full control over when animations run and can prevent SSR/client mismatches.
Implementation approach:
Best for: Enter/exit animations, route transitions, conditional UI animations, and any animation tied to user interaction.
Key advantage: This technique gives you precise control and makes animations predictable and testable.
Why it works: You get the best of both worlds - instant visual feedback from SSR and smooth animations after hydration.
Implementation approach:
Best for: Hero sections, above-the-fold animations, and cases where you want both SEO benefits and engaging animations.
Example scenario: Render a hero image at 80% opacity on the server, then fade it to 100% with a subtle scale effect after hydration.
This is where many developers make a costly mistake. When you move components to client-side rendering for animations, you risk hiding content from search engines. Here's how to keep your SEO intact while still having beautiful animations.
When you use approaches like:
'use client' in Next.js without proper structuressr: false in dynamic importsSearch engines may receive HTML that looks like this:
<!-- What Google sees -->
<div id="root"></div>
<script src="bundle.js"></script>
<!-- No content! -->
Your rankings will tank because there's no content for search engines to index.
Golden Rule: Always render your content on the server, then enhance it with animations on the client.
Keep your content in server components and only wrap animation logic in client components.
Next.js Example:
// app/page.tsx (Server Component - SEO friendly)
import AnimationWrapper from './AnimationWrapper';
export default function Page() {
return (
<div>
<h1>Your SEO-friendly heading</h1>
<p>This content is server-rendered and visible to search engines</p>
<AnimationWrapper>
<div className="feature-card">
<h2>Feature Title</h2>
<p>This content is ALSO server-rendered</p>
</div>
</AnimationWrapper>
</div>
);
}
// AnimationWrapper.tsx (Client Component)
'use client';
import { motion } from 'framer-motion';
export default function AnimationWrapper({ children }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
{children}
</motion.div>
);
}
What happens: The content renders on the server (SEO sees it), then Motion wraps it with animation behavior on the client.
Start with CSS for the initial state, enhance with JavaScript:
// Server Component - No 'use client' needed!
export default function HeroSection() {
return (
<section className="hero-initial">
<h1>Welcome to Our Site</h1>
<p>All this content is immediately visible to search engines</p>
</section>
);
}
// CSS handles the animation
// styles.css
.hero-initial {
animation: fadeInUp 0.8s ease-out forwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
SEO Impact: Zero. Content is fully rendered and visible in the initial HTML.
If you must use dynamic imports, always provide meaningful fallback content:
import dynamic from 'next/dynamic';
// Good: Fallback contains actual content
const AnimatedFeatures = dynamic(
() => import('./AnimatedFeatures'),
{
loading: () => (
<div className="features-grid">
<div className="feature">
<h3>Speed</h3>
<p>Lightning fast performance</p>
</div>
<div className="feature">
<h3>Security</h3>
<p>Bank-level encryption</p>
</div>
</div>
),
ssr: true // Keep SSR enabled!
}
);
// Bad: No content in fallback
const AnimatedFeatures = dynamic(
() => import('./AnimatedFeatures'),
{
loading: () => <div>Loading...</div>, // Search engines see nothing
ssr: false // Content not in initial HTML
}
);
Keep HTML semantic and SEO-friendly, use data attributes to trigger animations:
// Server Component (SEO-friendly)
export default function ProductGrid({ products }) {
return (
<div className="product-grid">
{products.map((product, index) => (
<article
key={product.id}
data-animate="fade-in"
data-delay={index * 100}
>
<h2>{product.name}</h2>
<p>{product.description}</p>
<span>${product.price}</span>
</article>
))}
</div>
);
}
// Separate client-side animation initialization
// animate.ts (loaded separately)
'use client';
if (typeof window !== 'undefined') {
document.querySelectorAll('[data-animate]').forEach((el, index) => {
const delay = el.getAttribute('data-delay') || 0;
setTimeout(() => {
el.classList.add('animated');
}, parseInt(delay));
});
}
In Angular, the same principles apply:
// SEO-friendly component
@Component({
selector: 'app-product-card',
template: `
<article class="product-card" [class.animated]="isAnimated">
<!-- Content always rendered server-side -->
<h2>{{ product.name }}</h2>
<p>{{ product.description }}</p>
<span>{{ product.price }}</span>
</article>
`
})
export class ProductCardComponent {
@Input() product: Product;
isAnimated = false;
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngAfterViewInit() {
// Only trigger animation on client
if (isPlatformBrowser(this.platformId)) {
setTimeout(() => this.isAnimated = true, 100);
}
}
}
Key point: The HTML structure and content are always rendered server-side. Only the animation state changes client-side.
Before deploying, verify your content is SEO-friendly:
1. View Page Source Right-click your page and select "View Page Source". You should see your actual content in the HTML, not just empty divs.
2. Use Google's Rich Results Test Visit: https://search.google.com/test/rich-results Paste your URL and verify Google can see your content.
3. Disable JavaScript In Chrome DevTools, disable JavaScript (Settings > Debugger > Disable JavaScript) and reload. Your content should still be visible.
4. Check Lighthouse SEO Score Run a Lighthouse audit. Your SEO score should remain high (90+) even with animations.
<a> tags, not click handlersAngular's powerful animation system needs special handling in SSR environments. Here's how to make it work smoothly.
1. Platform Detection
Angular provides the isPlatformBrowser and isPlatformServer utilities to detect the rendering environment:
import { Component, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'app-animated-card',
template: `<div [@fadeIn]="animationState">Content</div>`
})
export class AnimatedCardComponent {
animationState = 'void';
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Only trigger animations in browser
setTimeout(() => this.animationState = 'visible', 100);
}
}
}
2. Conditional Animation Imports
For heavy animation libraries, use dynamic imports:
import { Component, OnInit, PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export class HeroComponent implements OnInit {
private gsap: any;
constructor(@Inject(PLATFORM_ID) private platformId: Object) {}
async ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
const { gsap } = await import('gsap');
this.gsap = gsap;
this.initAnimations();
}
}
private initAnimations() {
this.gsap.from('.hero-title', {
y: 50,
opacity: 0,
duration: 1
});
}
}
3. Angular Animations with NoopAnimationsModule
Angular Universal automatically uses NoopAnimationsModule on the server. Ensure your animations degrade gracefully:
// In your app.server.module.ts
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [
AppModule,
ServerModule,
NoopAnimationsModule, // Automatically handles SSR
],
})
export class AppServerModule {}
4. Scroll-Based Animations in Angular
Create a directive for reusable scroll animations:
import { Directive, ElementRef, OnInit, OnDestroy, PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: '[appScrollReveal]'
})
export class ScrollRevealDirective implements OnInit, OnDestroy {
private observer: IntersectionObserver | null = null;
constructor(
private el: ElementRef,
@Inject(PLATFORM_ID) private platformId: Object
) {}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.el.nativeElement.classList.add('revealed');
}
});
});
this.observer.observe(this.el.nativeElement);
}
}
ngOnDestroy() {
if (this.observer) {
this.observer.disconnect();
}
}
}
5. Route Transition Animations
Handle route animations carefully in Universal:
import { trigger, transition, style, animate } from '@angular/animations';
export const routeAnimations = trigger('routeAnimations', [
transition('* <=> *', [
style({ opacity: 0 }),
animate('300ms', style({ opacity: 1 }))
])
]);
// In component
@Component({
selector: 'app-root',
template: `
<div [@routeAnimations]="outlet.isActivated ? outlet.activatedRoute : ''">
<router-outlet #outlet="outlet"></router-outlet>
</div>
`,
animations: [routeAnimations]
})
export class AppComponent {}
afterNextRender() (Angular 16+) for client-only code executionng serve and npm run dev:ssr to catch SSR issues earlyNext.js developers have fantastic animation options with proper SSR handling. Here's your complete guide.
1. Client-Only Component Wrapper
Create a utility for client-side only animations:
'use client'; // For Next.js 13+ with app directory
import { useEffect, useState } from 'react';
export default function ClientOnly({ children }: { children: React.ReactNode }) {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
return <>{children}</>;
}
2. Framer Motion with Next.js
Framer Motion is the go-to library for React animations. Here's the SEO-safe way to use it with SSR:
'use client';
import { motion, useReducedMotion } from 'framer-motion';
import { useEffect, useState } from 'react';
export default function AnimatedHero({ title, description }) {
const [isClient, setIsClient] = useState(false);
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
setIsClient(true);
}, []);
const variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: shouldReduceMotion ? 0 : 0.6 }
}
};
// IMPORTANT: Render content in both states for SEO
return (
<motion.div
initial="hidden"
animate={isClient ? "visible" : "hidden"}
variants={variants}
>
<h1>{title}</h1>
<p>{description}</p>
</motion.div>
);
}
// Usage in Server Component (SEO-friendly)
// app/page.tsx
export default function Page() {
return (
<div>
<AnimatedHero
title="Welcome to Our Site"
description="This content is server-rendered and SEO-friendly"
/>
</div>
);
}
SEO Note: Even though the component uses 'use client', Next.js still server-renders the initial HTML. The content is visible to search engines, and animations enhance it on the client.
3. Dynamic Imports for Animation Libraries (SEO-Safe)
Reduce initial bundle size with dynamic imports while keeping content SEO-friendly:
import dynamic from 'next/dynamic';
// SEO-Safe: Content renders on server, animations load separately
const AnimatedComponent = dynamic(
() => import('@/components/AnimatedComponent'),
{
ssr: true, // Keep this true for SEO!
loading: () => (
// Fallback shows actual content
<div className="component-fallback">
<h2>Our Services</h2>
<p>We provide world-class solutions</p>
</div>
)
}
);
// For heavy animation-only components
const ParticleBackground = dynamic(
() => import('@/components/ParticleBackground'),
{
ssr: false, // OK because this is decorative only, no SEO content
loading: () => null
}
);
export default function Page() {
return (
<main>
{/* Decorative animation - no SEO impact */}
<ParticleBackground />
{/* Content component - SEO-friendly */}
<AnimatedComponent />
</main>
);
}
Critical distinction: Only set ssr: false for purely decorative animations. Any component with meaningful content should have ssr: true.
4. Scroll Animations with Intersection Observer Hook
Create a reusable hook for scroll-triggered animations:
'use client';
import { useEffect, useRef, useState } from 'react';
export function useScrollReveal(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [threshold]);
return { ref, isVisible };
}
// Usage
export default function ScrollRevealCard() {
const { ref, isVisible } = useScrollReveal();
return (
<div
ref={ref}
className={`transition-all duration-700 ${
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10'
}`}
>
<h3>Card Content</h3>
</div>
);
}
5. Page Transitions in Next.js App Router
For smooth page transitions in Next.js 13+:
// app/template.tsx
'use client';
import { motion } from 'framer-motion';
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}
For Pages Router:
// pages/_app.tsx
import { AnimatePresence, motion } from 'framer-motion';
import { useRouter } from 'next/router';
function MyApp({ Component, pageProps }) {
const router = useRouter();
return (
<AnimatePresence mode="wait">
<motion.div
key={router.pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<Component {...pageProps} />
</motion.div>
</AnimatePresence>
);
}
'use client' directive for components with animations in the app directoryreact-spring for physics-based animations (also requires client-side handling)next build && next start to catch production SSR issuesRegardless of your framework, follow these guidelines:
will-change CSS property sparingly and only when neededprefers-reduced-motion for accessibilityrequestAnimationFrame for JavaScript animationsHydration Mismatch Errors: Always ensure server and client render the same initial state. Use mounting flags or platform detection.
Animation Library Size: Some libraries add 50-200KB to your bundle. Evaluate if you really need them or if CSS can do the job.
Animation on Every Element: Be selective. Animating 100 cards simultaneously will tank performance on mobile devices.
Forgetting About Accessibility: Always provide a way to disable animations for users who need it.
Hiding Content from Search Engines: This is the most damaging mistake. Never use ssr: false or client-only rendering for components with important content. Search engines need to see your content in the initial HTML. Always render content server-side and enhance with animations client-side.
Integrating animations into SSR applications doesn't have to be complicated. By understanding the server-client boundary and applying these five techniques, you can create engaging, performant animated experiences in both Angular Universal and Next.js.
The most important takeaway: Always prioritize SEO by rendering your content server-side. Animations should enhance the experience, not hide your content from search engines. Follow the "content first, animation second" principle, and your site will rank well while still providing a delightful user experience.
Start with CSS-based animations for quick wins, then progressively enhance with JavaScript libraries as needed. Always test your implementation in both development and production builds to catch SSR-specific issues early. And never forget to verify your content is visible in the page source before deploying.
Need help implementing these animations on your project? Our team specializes in building high-performance SSR applications with engaging animations that don't compromise on speed or SEO. Get in touch for a free consultation and let's bring your website to life without sacrificing your search rankings.
A seasoned systems architect with over a decade of experience guiding venture-backed startups and growth teams from 0-to-1 MVP delivery to scale. Specializes in cross-platform mobile architectures, resilient cloud infrastructure, and applied agentic AI integrations that drive measurable commercial impact.
Top Stories & AI Overviews
Prioritize our engineering & AI guides in your Google Top Stories & AI Overviews.
Let’s collaborate on your architecture roadmap, MVP sprint, or full product build with our senior team.







