How to Use CSS Animations and Transitions in Next.js 14 with Tailwind CSS
Use Tailwind CSS utility classes—transition-*, duration-*, and custom easing curves—to add CSS animations and transitions in Next.js 14 without writing raw CSS or importing JavaScript animation libraries.
The woosal1337/blog repository demonstrates a production-ready approach to CSS animations in Next.js. Built with the App Router and React 18, this blog uses Tailwind CSS as its sole styling engine, compiling declarative utility classes into native CSS transition, animation, and @keyframes rules. All effects are server-renderable, type-safe with TypeScript, and require zero runtime JavaScript overhead.
Why Tailwind CSS for Next.js Animations
Tailwind CSS generates static CSS at build time, which aligns perfectly with Next.js 14's server components. In tailwind.config.ts, the repository defines a custom easing curve called ease-house—a cubic-bezier(0.16, 1, 0.3, 1) timing function used consistently across interactive elements. This centralization ensures visual coherence without scattering magic values throughout the codebase.
The browser handles all interpolation natively. When you apply transition-opacity duration-200 ease-house, Tailwind emits three CSS rules that the browser optimizes during rendering, avoiding the layout thrashing common in JavaScript-driven animations.
Core Animation Patterns in the Repository
Transition Utilities for Interactive States
Every button, link, and hoverable element in components/ui/button.tsx follows the same pattern:
// components/ui/button.tsx (excerpt)
<button className="transition-colors duration-200 ease-house hover:bg-ink/10">
Click me
</button>
| Utility | Generated CSS Property | Purpose |
|---|---|---|
transition-colors |
transition-property: color, background-color, border-color, fill, stroke |
Animates color-related properties |
transition-opacity |
transition-property: opacity |
Fades elements in/out |
transition-transform |
transition-property: transform |
Scales, rotates, or translates elements |
duration-200 |
transition-duration: 200ms |
Sets animation length |
ease-house |
transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1) |
Applies custom easing from tailwind.config.ts |
Group-Hover for Complex Interactions
The group-hover variant enables child animations triggered by parent hover states. In components/ds/theme-switcher.tsx, this pattern creates coordinated effects without JavaScript event handlers:
// components/ds/theme-switcher.tsx (excerpt)
<div className="group flex items-center gap-2">
<span className="transition-opacity duration-200 ease-house group-hover:opacity-70">
Theme
</span>
<Icon className="transition-transform duration-200 ease-house group-hover:rotate-90" />
</div>
The group class establishes a hover context. Any child prefixed with group-hover: activates when the parent receives hover, allowing synchronized multi-element animations.
Practical Implementation Examples
Fade-In Hover Effect for Social Icons
The components/blocks/social-icons.tsx file implements subtle opacity transitions:
// components/blocks/social-icons.tsx (excerpt)
<a
href={social.url}
className="block p-2 rounded-lg transition-opacity duration-200 ease-house hover:opacity-60"
aria-label={social.name}
>
<SocialIcon />
</a>
How it works: The anchor defaults to full opacity. On hover, Tailwind's generated CSS triggers a 200ms transition to 60% opacity using the custom ease-house curve. The browser's compositor thread handles this smoothly at 60fps.
Animated Link Underline in MDX Content
The mdx-components.tsx file extends Markdown links with underline color transitions:
// mdx-components.tsx (excerpt)
<a
className="group/link whitespace-nowrap underline decoration-line underline-offset-[3px] transition-colors duration-200 ease-house hover:decoration-ink-soft"
>
{children}
</a>
Key techniques:
group/linkcreates a named group context specifically for this linktransition-colorsanimates the underline color viatext-decoration-colorhover:decoration-ink-softchanges underline color on hover
Card Component with Border and Overlay Transitions
Reusable block components combine multiple transition properties. This pattern from episode cards demonstrates layering effects:
// components/blocks/episode-card.tsx (excerpt)
<div
className="group relative block overflow-hidden rounded-2xl border border-line bg-[#0a0a0a] transition-colors duration-200 ease-house hover:border-line-strong"
>
<div
className="absolute inset-0 grid place-items-center opacity-0 transition-opacity duration-200 ease-house group-hover:opacity-100"
>
<PlayIcon />
</div>
{/* card content */}
</div>
Both border color and overlay opacity animate with identical timing, producing cohesive visual feedback.
Adding Custom Keyframe Animations
For animations beyond simple state transitions, extend tailwind.config.ts with custom @keyframes:
// tailwind.config.ts (excerpt)
import type { Config } from 'tailwindcss'
const config: Config = {
content: ['./app/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
transitionTimingFunction: {
'house': 'cubic-bezier(0.16, 1, 0.3, 1)',
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out forwards',
'slide-up': 'slideUp 0.4s ease-house forwards',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
},
},
},
plugins: [],
}
export default config
Consume custom animations with the animate-* prefix:
// usage in any component
<article className="animate-fade-in">
<header className="animate-slide-up animation-delay-100">
<h1>{title}</h1>
</header>
</article>
Performance Considerations
The repository's approach yields several performance benefits:
- Zero JavaScript overhead: CSS transitions run on the browser's compositor thread, not React's render cycle
- No layout thrashing: Tailwind generates
will-changehints implicitly through property-specific utilities - Server-side rendering safe: All animation classes are static strings, fully compatible with Next.js 14 server components
- Minimal bundle impact: Shared utilities deduplicate across components; custom keyframes compile once to CSS
Avoid animating width, height, top, left, or other layout-triggering properties. Stick to opacity and transform—the properties Tailwind's utilities target by default.
Key Files Reference
| File | Purpose |
|---|---|
tailwind.config.ts |
Custom easing (ease-house), keyframes, and animation definitions |
app/globals.css |
Tailwind directives and global style imports |
components/ui/button.tsx |
Reusable button with transition utilities |
components/blocks/social-icons.tsx |
Opacity hover transitions |
components/ds/theme-switcher.tsx |
Group-hover coordination patterns |
mdx-components.tsx |
Link underline animations |
Summary
- Use Tailwind utilities:
transition-*,duration-*, and custom easing intailwind.config.tscover most animation needs in Next.js 14 - Leverage group-hover: Create multi-element coordinated effects without JavaScript state management
- Extend via keyframes: Add custom
animationandkeyframesin the Tailwind config for complex sequences - Prefer opacity and transform: These properties maximize rendering performance and match Tailwind's built-in utilities
- Keep it declarative: Static class strings maintain server component compatibility and type safety
Frequently Asked Questions
Can I use CSS modules instead of Tailwind for animations in this repository?
Yes, but it would deviate from the established pattern. The woosal1337/blog repository uses Tailwind exclusively. If you require CSS modules, create .module.css files in app/ or components/, import them, and apply classes via the className prop. Note that you lose the centralized easing definitions and utility composition that Tailwind provides.
Why does the repository use a custom easing curve?
The ease-house curve (cubic-bezier(0.16, 1, 0.3, 1)) produces a "snap-to-finish" effect where animations start quickly and decelerate smoothly. Defined once in tailwind.config.ts, this curve ensures all transitions feel cohesive and branded. Adjust the bezier values to match your project's personality.
How do I animate on scroll or mount without Framer Motion?
For scroll-triggered animations, use the native CSS animation-timeline property with a polyfill, or mount a lightweight intersection observer in a client component. For mount animations, apply an animate-* class conditionally based on React state. The repository reserves framer-motion only for complex sequences like the GitHub stars badge in components/blocks/github-stars.tsx.
Do CSS transitions work with Next.js server components?
Yes. Tailwind utility classes are static strings evaluated at build time, making them fully compatible with React server components. The browser applies transitions when client-side interactivity (hover, focus) occurs—no hydration cost for the animation itself.
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 →