# How to Use CSS Animations and Transitions in Next.js 14 with Tailwind CSS

> Learn to add CSS animations and transitions in Next.js 14 using Tailwind CSS utility classes. Enhance your UI without raw CSS or JavaScript libraries.

- Repository: [Ege Chelebi/blog](https://github.com/woosal1337/blog)
- Tags: tutorial
- Published: 2026-08-06

---

**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`](https://github.com/woosal1337/blog/blob/main/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`](https://github.com/woosal1337/blog/blob/main/components/ui/button.tsx) follows the same pattern:

```tsx
// 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`](https://github.com/woosal1337/blog/blob/main/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`](https://github.com/woosal1337/blog/blob/main/components/ds/theme-switcher.tsx), this pattern creates coordinated effects without JavaScript event handlers:

```tsx
// 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`](https://github.com/woosal1337/blog/blob/main/components/blocks/social-icons.tsx) file implements subtle opacity transitions:

```tsx
// 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`](https://github.com/woosal1337/blog/blob/main/mdx-components.tsx) file extends Markdown links with underline color transitions:

```tsx
// 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/link`** creates a named group context specifically for this link
- **`transition-colors`** animates the underline color via `text-decoration-color`
- **`hover:decoration-ink-soft`** changes 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:

```tsx
// 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`](https://github.com/woosal1337/blog/blob/main/tailwind.config.ts) with custom `@keyframes`:

```tsx
// 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:

```tsx
// 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-change` hints 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`](https://github.com/woosal1337/blog/blob/main/tailwind.config.ts) | Custom easing (`ease-house`), keyframes, and animation definitions |
| [`app/globals.css`](https://github.com/woosal1337/blog/blob/main/app/globals.css) | Tailwind directives and global style imports |
| [`components/ui/button.tsx`](https://github.com/woosal1337/blog/blob/main/components/ui/button.tsx) | Reusable button with transition utilities |
| [`components/blocks/social-icons.tsx`](https://github.com/woosal1337/blog/blob/main/components/blocks/social-icons.tsx) | Opacity hover transitions |
| [`components/ds/theme-switcher.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/theme-switcher.tsx) | Group-hover coordination patterns |
| [`mdx-components.tsx`](https://github.com/woosal1337/blog/blob/main/mdx-components.tsx) | Link underline animations |

---

## Summary

- **Use Tailwind utilities**: `transition-*`, `duration-*`, and custom easing in [`tailwind.config.ts`](https://github.com/woosal1337/blog/blob/main/tailwind.config.ts) cover most animation needs in Next.js 14
- **Leverage group-hover**: Create multi-element coordinated effects without JavaScript state management
- **Extend via keyframes**: Add custom `animation` and `keyframes` in 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`](https://github.com/woosal1337/blog/blob/main/.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`](https://github.com/woosal1337/blog/blob/main/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`](https://github.com/woosal1337/blog/blob/main/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.