¿Qué es?
Una “split landing page” es una portada con dos mitades a pantalla completa. Al hover sobre una mitad, esta crece y atrae la atención; al salir, todo vuelve al 50/50.
Conceptos clave que aprenderás
-
Layout con Flexbox para dividir el viewport en dos paneles.
-
Transiciones y pseudo-clases para animar el cambio de anchos.
-
Clases dinámicas con JS (.hover-left / .hover-right) para controlar estados. (Patrón usado en múltiples implementaciones públicas del reto.)
Código (3 archivos separados)
1) index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Split Landing Page</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="split">
<section class="pane left">
<div class="overlay">
<h1>Opción A</h1>
<a class="btn" href="#a">Explorar</a>
</div>
</section>
<section class="pane right">
<div class="overlay">
<h1>Opción B</h1>
<a class="btn" href="#b">Explorar</a>
</div>
</section>
</main>
<script src="script.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Split Landing Page</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="split">
<section class="pane left">
<div class="overlay">
<h1>Opción A</h1>
<a class="btn" href="#a">Explorar</a>
</div>
</section>
<section class="pane right">
<div class="overlay">
<h1>Opción B</h1>
<a class="btn" href="#b">Explorar</a>
</div>
</section>
</main>
<script src="script.js"></script>
</body>
</html>
2) Style.css
* { box-sizing: border-box; }
:root {
--speed: 600ms;
--radius: 24px;
--gap: 8px;
}
html, body {
height: 100%;
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: #0f1115;
color: #f5f7fa;
}
/* Contenedor “split” a pantalla completa */
.split {
display: flex;
height: 100vh;
gap: var(--gap);
padding: var(--gap);
}
/* Panel base */
.pane {
flex: 1 1 50%;
position: relative;
border-radius: var(--radius);
overflow: hidden;
transition: flex-basis var(--speed) ease, transform var(--speed) ease;
background-size: cover;
background-position: center;
}
/* Imágenes de ejemplo (cámbialas por las tuyas) */
.left { background-image: url("https://picsum.photos/id/1018/1600/900"); }
.right { background-image: url("https://picsum.photos/id/1025/1600/900"); }
/* Capa oscura y contenido centrado */
.overlay {
position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,.25), rgba(0,0,0,.55));
display: grid; place-content: center; text-align: center;
padding: 24px;
}
h1 { margin: 0 0 16px; font-size: clamp(28px, 4vw, 56px); }
.btn {
display: inline-block;
padding: 12px 20px;
border-radius: 999px;
border: 2px solid rgba(255,255,255,.85);
color: #fff; text-decoration: none;
transition: transform 180ms ease, background-color 180ms ease, border-color 180ms ease;
backdrop-filter: blur(2px);
}
.btn:hover { transform: translateY(-2px); background: rgba(255,255,255,.08); border-color: #fff; }
/* Estados de hover activados por clases JS */
.split.hover-left .left { flex-basis: 75%; }
.split.hover-left .right { flex-basis: 25%; }
.split.hover-right .right { flex-basis: 75%; }
.split.hover-right .left { flex-basis: 25%; }
/* Sutileza: zoom leve del panel activo */
.split.hover-left .left,
.split.hover-right .right { transform: scale(1.02); }
/* Responsive (táctiles): apilar en vertical si el alto es limitado */
@media (max-width: 760px) {
.split { flex-direction: column; }
}
* { box-sizing: border-box; }
:root {
--speed: 600ms;
--radius: 24px;
--gap: 8px;
}
html, body {
height: 100%;
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: #0f1115;
color: #f5f7fa;
}
/* Contenedor “split” a pantalla completa */
.split {
display: flex;
height: 100vh;
gap: var(--gap);
padding: var(--gap);
}
/* Panel base */
.pane {
flex: 1 1 50%;
position: relative;
border-radius: var(--radius);
overflow: hidden;
transition: flex-basis var(--speed) ease, transform var(--speed) ease;
background-size: cover;
background-position: center;
}
/* Imágenes de ejemplo (cámbialas por las tuyas) */
.left { background-image: url("https://picsum.photos/id/1018/1600/900"); }
.right { background-image: url("https://picsum.photos/id/1025/1600/900"); }
/* Capa oscura y contenido centrado */
.overlay {
position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(0,0,0,.25), rgba(0,0,0,.55));
display: grid; place-content: center; text-align: center;
padding: 24px;
}
h1 { margin: 0 0 16px; font-size: clamp(28px, 4vw, 56px); }
.btn {
display: inline-block;
padding: 12px 20px;
border-radius: 999px;
border: 2px solid rgba(255,255,255,.85);
color: #fff; text-decoration: none;
transition: transform 180ms ease, background-color 180ms ease, border-color 180ms ease;
backdrop-filter: blur(2px);
}
.btn:hover { transform: translateY(-2px); background: rgba(255,255,255,.08); border-color: #fff; }
/* Estados de hover activados por clases JS */
.split.hover-left .left { flex-basis: 75%; }
.split.hover-left .right { flex-basis: 25%; }
.split.hover-right .right { flex-basis: 75%; }
.split.hover-right .left { flex-basis: 25%; }
/* Sutileza: zoom leve del panel activo */
.split.hover-left .left,
.split.hover-right .right { transform: scale(1.02); }
/* Responsive (táctiles): apilar en vertical si el alto es limitado */
@media (max-width: 760px) {
.split { flex-direction: column; }
}
3) Script.js
const split = document.querySelector('.split');
const left = document.querySelector('.left');
const right = document.querySelector('.right');
function onEnterLeft() { split.classList.add('hover-left'); }
function onLeaveLeft() { split.classList.remove('hover-left'); }
function onEnterRight() { split.classList.add('hover-right'); }
function onLeaveRight() { split.classList.remove('hover-right'); }
left.addEventListener('mouseenter', onEnterLeft);
left.addEventListener('mouseleave', onLeaveLeft);
right.addEventListener('mouseenter', onEnterRight);
right.addEventListener('mouseleave', onLeaveRight);
/* Soporte básico táctil: al tocar, expandir esa mitad */
left.addEventListener('touchstart', onEnterLeft, { passive: true });
right.addEventListener('touchstart', onEnterRight, { passive: true });
document.addEventListener('touchend', () => {
split.classList.remove('hover-left', 'hover-right');
}, { passive: true });
const split = document.querySelector('.split');
const left = document.querySelector('.left');
const right = document.querySelector('.right');
function onEnterLeft() { split.classList.add('hover-left'); }
function onLeaveLeft() { split.classList.remove('hover-left'); }
function onEnterRight() { split.classList.add('hover-right'); }
function onLeaveRight() { split.classList.remove('hover-right'); }
left.addEventListener('mouseenter', onEnterLeft);
left.addEventListener('mouseleave', onLeaveLeft);
right.addEventListener('mouseenter', onEnterRight);
right.addEventListener('mouseleave', onLeaveRight);
/* Soporte básico táctil: al tocar, expandir esa mitad */
left.addEventListener('touchstart', onEnterLeft, { passive: true });
right.addEventListener('touchstart', onEnterRight, { passive: true });
document.addEventListener('touchend', () => {
split.classList.remove('hover-left', 'hover-right');
}, { passive: true });
El patrón de añadir/quitar clases en mouseenter/mouseleave (o touch) es el enfoque más común para este mini-proyecto y clones, porque deja la animación en manos del CSS.
Personaliza en 30 segundos
-
Cambia las fotos: reemplaza las URLs de background-image por tus propias imágenes (CDN o ruta local).
-
Texto y botón: edita los <h1> y .btn.
-
Velocidad y proporciones: ajusta --speed y los flex-basis: 75% / 25% en los estados .hover-left / .hover-right.
Accesibilidad & UX rápidos
-
Mantén contraste alto en el overlay para legibilidad.
-
Añade :focus-visible a .btn si tendrá navegación por teclado.
-
En móviles el layout se apila (media query), y el toque activa la expansión un instante para evitar “estado pegado”.
Ejemplo
Finalmente tu implementación debería verse similar a esta