Cómo funciona la animación de ondas 3D en CSS

En este tutorial aprenderás a construir un carrusel con sensación de profundidad 3D utilizando propiedades avanzadas como perspective, translateZ() y rotaciones en el eje Y, sin necesidad de JavaScript.

Objetivo: Crear un componente reutilizable e interactivo que puedas integrar en cualquier proyecto web, optimizado para todos los dispositivos y accesible para todos los usuarios.

Al final dominarás técnicas avanzadas de CSS que te permitirán crear experiencias visuales impactantes manteniendo el rendimiento y la accesibilidad.

HTML base para el efecto 3D en CSS

El HTML debe ser semántico y accesible. Utilizamos una estructura jerárquica clara con elementos focalizables por teclado.

<section class="showcase">
  <div class="wrapper">
    <div class="items">
      <div class="item" tabindex="0" 
           style="background-image:url(https://elsaltoweb.es/wp-content/uploads/2025/05/zapatillas4.webp)"
           aria-label="Zapatillas deportivas rojas"></div>
      <div class="item" tabindex="0" 
           style="background-image:url(https://elsaltoweb.es/wp-content/uploads/2025/05/zapatillas3-scaled.webp)"
           aria-label="Zapatillas urbanas azules"></div>
      <div class="item" tabindex="0" 
           style="background-image:url(https://elsaltoweb.es/wp-content/uploads/2025/05/zapatillas2-scaled.webp)"
           aria-label="Zapatillas running negras"></div>
      <div class="item" tabindex="0" 
           style="background-image:url(https://elsaltoweb.es/wp-content/uploads/2025/07/camiseta-argentina-2024.webp)"
           aria-label="Camiseta Argentina 2024"></div>
      <div class="item" tabindex="0" 
           style="background-image:url(https://elsaltoweb.es/wp-content/uploads/2025/05/zapatillas1-scaled.webp)"
           aria-label="Zapatillas casual blancas"></div>
    </div>
  </div>
</section>

CSS para animar ondas 3D

Establecemos las variables CSS, reset básico y la disposición del carrusel antes de añadir los efectos 3D.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --index: calc(1vw + 1vh);
  --transition: cubic-bezier(.1, .7, 0, 1);
  --duration: 1.25s;
}

body {
  background-color: #141414;
  font-family: 'Inter', sans-serif;
}

.wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 50vh;
  padding: 2rem;
}

.items {
  display: flex;
  gap: 0.4rem;
  perspective: calc(var(--index) * 35);
}

.item {
  width: calc(var(--index) * 3);
  height: calc(var(--index) * 12);
  background-color: #222;
  background-size: cover;
  background-position: center;
  cursor: pointer;
  filter: grayscale(1) brightness(.5);
  transition: 
    transform var(--duration) var(--transition), 
    filter 3s var(--transition), 
    width var(--duration) var(--transition);
  will-change: transform;
  border-radius: 12px;
  overflow: hidden;
  position: relative;
}

.item::before, 
.item::after {
  content: '';
  position: absolute;
  height: 100%;
  width: 20px;
  background: inherit;
  filter: brightness(0.3);
}

.item::before {
  right: calc(var(--index) * -1);
  transform: rotateY(-45deg);
  transform-origin: right;
}

.item::after {
  left: calc(var(--index) * -1);
  transform: rotateY(45deg);
  transform-origin: left;
}

/* Estados interactivos */
.item:hover,
.item:focus {
  filter: inherit;
  transform: translateZ(calc(var(--index) * 10));
  outline: 2px solid #00d4ff;
  outline-offset: 4px;
}

/* Efectos en tarjetas adyacentes - Lado derecho */
.item:hover + *,
.item:focus + * {
  filter: inherit;
  transform: translateZ(calc(var(--index) * 8.5)) rotateY(35deg);
  z-index: -1;
}

.item:hover + * + *,
.item:focus + * + * {
  filter: inherit;
  transform: translateZ(calc(var(--index) * 5.6)) rotateY(40deg);
  z-index: -2;
}

/* Efectos en tarjetas adyacentes - Lado izquierdo */
.item:has(+ :hover),
.item:has(+ :focus) {
  filter: inherit;
  transform: translateZ(calc(var(--index) * 8.5)) rotateY(-35deg);
}

.item:has(+ * + :hover),
.item:has(+ * + :focus) {
  filter: inherit;
  transform: translateZ(calc(var(--index) * 5.6)) rotateY(-40deg);
}

/* Estado activo/expandido */
.item:active,
.item:focus-visible {
  width: 28vw;
  filter: inherit;
  z-index: 100;
  transform: translateZ(calc(var(--index) * 10));
  margin: 0 0.45vw;
}

4. Profundidad y perspectiva

La magia del efecto 3D reside en la correcta aplicación de perspective y translateZ(). Estos crean la ilusión de que las tarjetas flotan en un espacio tridimensional.

🎯 Conceptos clave

  • Perspective: Define la distancia del punto de vista al plano Z=0
  • TranslateZ: Mueve elementos hacia/desde el usuario en el eje Z
  • RotateY: Rota elementos alrededor del eje vertical
  • Transform-origin: Establece el punto de rotación
/* Contenedor con perspectiva */
.items {
  perspective: calc(var(--index) * 35);
  perspective-origin: center center;
}

/* Tarjeta principal en hover */
.item:hover {
  transform: translateZ(calc(var(--index) * 10)) scale(1.02);
  filter: brightness(1) contrast(1.1) saturate(1.2);
  box-shadow: 
    0 20px 40px rgba(0, 0, 0, 0.3),
    0 0 0 2px rgba(0, 212, 255, 0.5);
}

/* Creación de grosor 3D */
.item::before {
  background: linear-gradient(
    90deg, 
    rgba(0, 0, 0, 0.4) 0%, 
    rgba(0, 0, 0, 0.1) 100%
  );
  transform: rotateY(-45deg) translateZ(-2px);
}

Pro Tip: Ajusta el valor de perspective para controlar la intensidad del efecto 3D. Valores menores = más dramático, valores mayores = más sutil.

5. Empuje de tarjetas adyacentes

El selector :has() nos permite crear un efecto de "empuje" donde las tarjetas vecinas reaccionan cuando una está en hover, creando un abanico 3D natural.

/* Detección de hermano en hover (lado izquierdo) */
.item:has(+ :hover) {
  filter: inherit;
  transform: 
    translateZ(calc(var(--index) * 8.5)) 
    rotateY(-35deg) 
    scale(0.95);
  transition-delay: 0.1s;
}

/* Efecto cascada hacia la izquierda */
.item:has(+ * + :hover) {
  transform: 
    translateZ(calc(var(--index) * 5.6)) 
    rotateY(-40deg) 
    scale(0.9);
  transition-delay: 0.2s;
}

.item:has(+ * + * + :hover) {
  transform: 
    translateZ(calc(var(--index) * 2.5)) 
    rotateY(-30deg) 
    scale(0.85);
  transition-delay: 0.3s;
}

/* Efecto cascada hacia la derecha */
.item:hover + * {
  transform: 
    translateZ(calc(var(--index) * 8.5)) 
    rotateY(35deg) 
    scale(0.95);
}

.item:hover + * + * {
  transform: 
    translateZ(calc(var(--index) * 5.6)) 
    rotateY(40deg) 
    scale(0.9);
}

6. Diseño responsivo

En dispositivos móviles, el efecto 3D puede ser problemático. Adaptamos la experiencia para que sea táctil y fluida.

/* Tablet y móviles */
@media (max-width: 1024px) {
  .items {
    perspective: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    gap: 1rem;
    padding: 0 1rem;
  }
  
  .item {
    flex-shrink: 0;
    width: 280px;
    height: 400px;
    scroll-snap-align: center;
    transform: none !important;
    filter: none !important;
  }
  
  .item:hover,
  .item:focus {
    transform: scale(1.05) !important;
  }
}

/* Móviles pequeños */
@media (max-width: 480px) {
  .item {
    width: 240px;
    height: 320px;
  }
  
  .wrapper {
    height: 40vh;
    padding: 1rem;
  }
}

/* Reduce movimiento si el usuario lo prefiere */
@media (prefers-reduced-motion: reduce) {
  .item {
    transition: filter 0.3s ease;
  }
  
  .item:hover,
  .item:focus {
    transform: none !important;
  }
}

7. Accesibilidad

Un carrusel accesible debe ser navegable por teclado, compatible con lectores de pantalla y respetar las preferencias del usuario.

/* Foco visible y claro */
.item:focus-visible {
  outline: 3px solid #00d4ff;
  outline-offset: 4px;
  border-radius: 16px;
}

/* Estados ARIA */
.item[aria-selected="true"] {
  transform: translateZ(calc(var(--index) * 10));
  filter: inherit;
}

/* Respeto por preferencias de movimiento */
@media (prefers-reduced-motion: reduce) {
  .item {
    transition: 
      filter 0.2s ease,
      transform 0.2s ease;
  }
  
  .item:hover {
    transform: scale(1.02);
  }
}

/* Alto contraste */
@media (prefers-contrast: high) {
  .item {
    border: 2px solid white;
  }
  
  .item:focus {
    outline: 4px solid yellow;
  }
}

Consejos de rendimiento y compatibilidad

Para mantener 60fps constantes, es crucial optimizar las animaciones y recursos.

🚀 Técnicas de optimización

  • will-change: Prepara el navegador para cambios específicos
  • transform3d: Fuerza aceleración por hardware
  • Composite layers: Aísla elementos que cambian frecuentemente
  • Debouncing: Limita la frecuencia de eventos costosos
/* Optimizaciones de rendimiento */
.item {
  will-change: transform, filter;
  backface-visibility: hidden;
  transform-style: preserve-3d;
  /* Fuerza composite layer */
  transform: translate3d(0, 0, 0);
}

/* Limpia will-change cuando no se necesita */
.items:not(:hover) .item {
  will-change: auto;
}

/* Optimización para imágenes */
.item {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  /* Suavizado de imágenes */
  image-rendering: -webkit-optimize-contrast;
}

/* Contenido crítico primero */
.item:nth-child(-n+3) {
  background-image: url(imagen-critica.webp);
}

/* Lazy loading para el resto */
.item:nth-child(n+4) {
  background-image: none;
}

.item:nth-child(n+4)[data-loaded] {
  background-image: var(--lazy-bg);
}

Performance Tip: Usa herramientas como Chrome DevTools Performance tab para identificar cuellos de botella y validar que tus animaciones se ejecuten a 60fps.

Aprende más sobre propiedades CSS en la documentación de MDN.

9. Preguntas frecuentes

❓ ¿Necesito JavaScript para este carrusel?

No para el efecto base. Todo está hecho con CSS puro. Solo necesitarías JS si quieres añadir navegación automática, botones de control o lazy loading avanzado.

❓ ¿Funciona en todos los navegadores?

El efecto base funciona en todos los navegadores modernos. El selector :has() (para el efecto cascada) requiere Safari 15.4+, Chrome 105+, Firefox 121+. En navegadores antiguos simplemente no verás el efecto de tarjetas adyacentes.

❓ ¿Cómo añado más tarjetas dinámicamente?

Simplemente añade más elementos .item al contenedor .items. El CSS se aplicará automáticamente. Para lazy loading, usa el atributo data-src y carga las imágenes con JavaScript cuando sean visibles.

❓ ¿Puedo personalizar los colores y tamaños?

¡Absolutamente! Modifica las variables CSS en :root para personalizar colores, tamaños y timing. Usa --index para escalar todo proporcionalmente.

❓ ¿Qué pasa en dispositivos táctiles?

En tablets y móviles, el carrusel se convierte automáticamente en un scroll horizontal con scroll-snap, manteniendo una experiencia fluida y nativa.