Carrusel 3D con CSS puro
En esta guía aprenderás a crear una animación de ondas 3D en CSS usando solo HTML y CSS puro, sin JavaScript.
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.