⏱️ Lectura: 9 min
Durante años, crear formas complejas en la web significaba recurrir a SVG, imágenes PNG con transparencia o hacks con border-radius y pseudo-elementos. Con la llegada de clip-path: shape(), CSS nos da una herramienta nativa para definir cualquier forma geométrica directamente desde la hoja de estilos.
📑 En este artículo
- Qué es clip-path: shape()
- Sintaxis básica
- Primer ejemplo: un diamante
- Segundo ejemplo: una gota de agua
- El poder real: custom properties
- Ejemplo avanzado: pieza de puzzle con CSS
- Armando el puzzle completo
- Mostrando una imagen dentro del puzzle
- Progressive enhancement con @supports
- Usos creativos más allá del puzzle
- Compatibilidad de navegadores
- Preguntas frecuentes
- Referencias
En este artículo vas a aprender qué es shape(), cómo funciona, y cómo construir desde formas simples hasta piezas de puzzle interactivas — todo con CSS puro y un mínimo de JavaScript.
🧪 Estado actual:clip-path: shape()es parte de CSS Shapes Level 2. Disponible en Chrome 131+, Edge 131+ y Safari 18.2+. Firefox aún no lo soporta (abril 2026). Usa@supportspara progressive enhancement.
Qué es clip-path: shape()
clip-path no es nuevo — lleva años permitiendo recortar elementos con formas básicas como circle(), ellipse(), inset() y polygon(). Pero todas tienen limitaciones:
- polygon() — solo líneas rectas, sin curvas
- circle() / ellipse() — solo formas redondeadas básicas
- path() — acepta SVG path data, pero no soporta unidades CSS ni custom properties
shape() resuelve todo esto. Es como path() pero con sintaxis CSS nativa: acepta porcentajes, calc(), custom properties y todas las unidades CSS. Es la pieza que faltaba.
Sintaxis básica
La función shape() define un camino con comandos de dibujo. Empieza con un punto de origen y traza líneas o curvas:
.forma {
clip-path: shape(
from 0% 0%, /* punto de inicio */
line to 100% 0%, /* línea al borde derecho */
line to 100% 100%,/* línea abajo */
line to 0% 100%, /* línea a la izquierda */
close /* cierra el camino al punto de inicio */
);
}
Este ejemplo dibuja un rectángulo — no muy útil, pero muestra la estructura. Los comandos disponibles son:
from X Y— punto de inicio (obligatorio, siempre primero)line to X Y— línea recta hasta un puntocurve to X Y using CP1X CP1Y— curva cuadrática (un punto de control)curve to X Y using CP1X CP1Y / CP2X CP2Y— curva cúbica (dos puntos de control)hline to X— línea horizontalvline to Y— línea verticalclose— cierra el camino al punto de inicio
Primer ejemplo: un diamante
Empecemos con algo simple — un diamante (rombo):
.diamante {
width: 200px;
aspect-ratio: 1;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
clip-path: shape(
from 50% 0%,
line to 100% 50%,
line to 50% 100%,
line to 0% 50%,
close
);
}
Cuatro puntos, cuatro líneas, un diamante. Nada que no pudieras hacer con polygon(). La magia viene cuando agregamos curvas.
Segundo ejemplo: una gota de agua
Con curve to podemos crear formas orgánicas imposibles con polygon():
.gota {
width: 150px;
height: 200px;
background: #0ea5e9;
clip-path: shape(
from 50% 0%,
curve to 100% 60% using 85% 10%,
curve to 50% 100% using 100% 85%,
curve to 0% 60% using 0% 85%,
curve to 50% 0% using 15% 10%,
close
);
}
Cada curve to define el punto destino y un punto de control (using) que determina la curvatura. Es el mismo concepto que las curvas Bézier de Illustrator o Figma, pero en CSS.
💡 Tip: Para visualizar cómo funcionan los puntos de control, piensa en ellos como imanes que “jalan” la línea hacia sí. Cuanto más lejos esté el punto de control del camino recto, más pronunciada será la curva.
El poder real: custom properties
A diferencia de path(), shape() acepta variables CSS y calc(). Esto permite crear formas parametrizables:
.estrella {
--puntas: 5;
--radio-ext: 50%;
--radio-int: 20%;
width: 200px;
aspect-ratio: 1;
background: #f59e0b;
clip-path: shape(
from 50% calc(50% - var(--radio-ext)),
/* ... puntos calculados dinámicamente */
close
);
}
Puedes cambiar --radio-ext y --radio-int con JavaScript, en un hover, o incluso con una animación CSS — y la forma se actualiza en tiempo real. Esto era imposible con path().
Ejemplo avanzado: pieza de puzzle con CSS
Ahora viene lo interesante. Una pieza de puzzle es un cuadrado con cuatro bordes, donde cada borde puede tener:
- Pestaña (tab) — protuberancia hacia afuera
- Encaje (socket) — cavidad hacia adentro
- Plano — borde recto (para los extremos del puzzle)
Estructura base
El cuadrado interior ocupa el 60% central de la pieza. El 20% restante en cada lado es el espacio para las pestañas y encajes:
.pieza {
position: relative;
width: 160px;
aspect-ratio: 1;
}
.pieza::after {
content: "";
position: absolute;
inset: 0;
background: #e2e8f0;
clip-path: shape(
from 20% 20%,
line to 80% 20%, /* borde superior */
line to 80% 80%, /* borde derecho */
line to 20% 80%, /* borde inferior */
close /* borde izquierdo */
);
}
Agregando pestañas con curvas
Para convertir un borde recto en una pestaña, reemplazamos la línea recta por una secuencia de líneas y curvas que dibujan la protuberancia:
/* Borde superior con pestaña hacia arriba */
.pieza::after {
clip-path: shape(
from 20% 20%,
/* --- borde superior con pestaña --- */
line to 40% 20%,
curve to 45% 5% using 40% 12%,
curve to 55% 5% using 50% 0%,
curve to 60% 20% using 60% 12%,
line to 80% 20%,
/* --- resto del cuadrado --- */
line to 80% 80%,
line to 20% 80%,
close
);
}
La pestaña se dibuja en tres curvas: la subida izquierda, el arco superior y la bajada derecha. Juntas forman el “nudo” clásico de una pieza de puzzle.
Parametrizando con custom properties
Para hacer piezas flexibles, usamos variables CSS para controlar cada borde independientemente:
.pieza {
/* Tamaño de la pestaña: negativo = pestaña, positivo = encaje, 0 = plano */
--top-size: -15%;
--right-size: 12%;
--bottom-size: -10%;
--left-size: 0%;
/* Desplazamiento lateral de la pestaña (-8% a 8%) */
--top-offset: 2%;
--right-offset: -3%;
--bottom-offset: 5%;
--left-offset: 0%;
}
El truco clave: cuando --top-size es negativo, la forma se extiende hacia afuera (pestaña). Cuando es positivo, se hunde hacia adentro (encaje). Cuando es 0, el borde queda plano.
📝 Nota: Para que dos piezas encajen, la pieza de la derecha necesita un--left-sizecon el signo opuesto al--right-sizede la pieza de la izquierda. Si una tiene-15%(pestaña), la vecina necesita15%(encaje).
Armando el puzzle completo
Con un poco de JavaScript generamos un grid de piezas donde cada una encaja con sus vecinas:
const filas = 4;
const columnas = 6;
const contenedor = document.querySelector('.puzzle');
contenedor.style.setProperty('--filas', filas);
contenedor.style.setProperty('--columnas', columnas);
for (let y = 0; y < filas; y++) {
for (let x = 0; x < columnas; x++) {
const pieza = document.createElement('div');
pieza.classList.add('pieza');
// Borde superior: copiar del vecino de arriba (invertido) o plano si es la primera fila
if (y === 0) {
pieza.dataset.topSize = 0;
pieza.dataset.topOffset = 0;
} else {
const vecino = contenedor.children[(y - 1) * columnas + x];
pieza.dataset.topSize = -Number(vecino.dataset.bottomSize);
pieza.dataset.topOffset = vecino.dataset.bottomOffset;
}
// Borde izquierdo: copiar del vecino de la izquierda (invertido)
if (x === 0) {
pieza.dataset.leftSize = 0;
pieza.dataset.leftOffset = 0;
} else {
const vecino = contenedor.children[y * columnas + (x - 1)];
pieza.dataset.leftSize = -Number(vecino.dataset.rightSize);
pieza.dataset.leftOffset = vecino.dataset.rightOffset;
}
// Bordes derecho e inferior: generar al azar
pieza.dataset.rightSize = x === columnas - 1
? 0
: (Math.random() * 10 + 10) * (Math.random() < 0.5 ? -1 : 1);
pieza.dataset.rightOffset = x === columnas - 1
? 0
: Math.random() * 16 - 8;
pieza.dataset.bottomSize = y === filas - 1
? 0
: (Math.random() * 10 + 10) * (Math.random() < 0.5 ? -1 : 1);
pieza.dataset.bottomOffset = y === filas - 1
? 0
: Math.random() * 16 - 8;
pieza.style.setProperty('--x', x);
pieza.style.setProperty('--y', y);
contenedor.appendChild(pieza);
}
}
La lógica es sencilla:
- Primera fila y primera columna — bordes exteriores planos (
0) - Última fila y última columna — bordes exteriores planos
- Bordes interiores — tamaño aleatorio entre 10% y 20%, dirección aleatoria (pestaña o encaje)
- Vecinos — copian el valor del borde adyacente multiplicado por
-1para que encaje
Leyendo los data attributes desde CSS
Con la función attr() tipada podemos leer los valores directamente en CSS sin necesidad de style.setProperty() para cada variable:
.pieza {
--top-size: calc(attr(data-top-size type(<number>)) * 1%);
--top-offset: calc(attr(data-top-offset type(<number>)) * 1%);
--right-size: calc(attr(data-right-size type(<number>)) * 1%);
--right-offset: calc(attr(data-right-offset type(<number>)) * 1%);
--bottom-size: calc(attr(data-bottom-size type(<number>)) * 1%);
--bottom-offset: calc(attr(data-bottom-offset type(<number>)) * 1%);
--left-size: calc(attr(data-left-size type(<number>)) * 1%);
--left-offset: calc(attr(data-left-offset type(<number>)) * 1%);
}
⚠️ Importante:attr()con tipado (type(<number>)) es una feature de CSS Values Level 5. Actualmente solo disponible en Chrome 133+ con flag habilitado. Para producción, usastyle.setProperty()por ahora.
Mostrando una imagen dentro del puzzle
El toque final: en vez de colores sólidos, cada pieza muestra un fragmento de la misma imagen:
.pieza::after {
background-image: url('imagen.jpg');
background-size:
calc(var(--columnas) * 60%)
calc(var(--filas) * 60%);
background-position:
calc(100% * (var(--x) - 1/3) / (var(--columnas) - 5/3))
calc(100% * (var(--y) - 1/3) / (var(--filas) - 5/3));
}
La matemática parece intimidante pero tiene lógica:
- background-size — la imagen se escala al tamaño total del puzzle. El
60%compensa que cada pieza usa solo el 60% central como cuadrado base - background-position — calcula la posición de cada fragmento. El
-1/3ajusta por el espacio extra de las pestañas, y el-5/3en el denominador normaliza la distribución
El resultado: cada pieza muestra su fragmento exacto de la imagen, y al juntarlas forman la imagen completa.
Progressive enhancement con @supports
Como shape() aún no tiene soporte universal, siempre deberías usar @supports:
/* Fallback: esquinas redondeadas simples */
.pieza {
border-radius: 8px;
overflow: hidden;
}
/* Si el navegador soporta shape(), aplicar la geometría compleja */
@supports (clip-path: shape(from 0% 0%, line to 100% 100%)) {
.pieza {
border-radius: 0;
overflow: visible;
}
.pieza::after {
clip-path: shape(/* ... */);
}
}
Usos creativos más allá del puzzle
Las piezas de puzzle son el ejemplo más visual, pero shape() abre posibilidades para:
- Transiciones de página — formas que se expanden o contraen al navegar
- Máscaras de imagen — recortes orgánicos para galerías de fotos
- Loaders animados — formas que mutan con
@keyframes - Bordes decorativos — ondas, dientes de sierra, patrones geométricos
- Reveals en hover — mostrar contenido con formas que se expanden
Compatibilidad de navegadores
| Navegador | clip-path: shape() | attr() tipado |
|---|---|---|
| Chrome 131+ | ✅ | 🚧 Flag |
| Edge 131+ | ✅ | 🚧 Flag |
| Safari 18.2+ | ✅ | ❌ |
| Firefox | ❌ | ❌ |
Fuente: Can I Use — CSS shape()
Preguntas frecuentes
¿Cuál es la diferencia entre shape() y polygon()?
polygon() solo permite líneas rectas entre puntos. shape() soporta curvas Bézier (cuadráticas y cúbicas), líneas horizontales/verticales, y lo más importante: acepta calc(), var() y todas las unidades CSS. Es la evolución natural de polygon().
¿Puedo animar shape()?
Sí, pero con limitaciones. Puedes animar las custom properties que alimentan a shape() usando @property registrado. La forma se interpola si ambos estados tienen la misma cantidad de comandos.
¿shape() reemplaza a SVG?
Para formas usadas como recorte o decoración CSS, sí. Para gráficos complejos, íconos detallados o ilustraciones, SVG sigue siendo la mejor opción. Son herramientas complementarias.
Referencias
- CSS Shapes Level 2 — shape() specification (W3C)
- MDN Web Docs — shape()
- Can I Use — CSS shape()
- Let’s Get Puzzled! — Amit Sheen (Frontend Masters)
Síguenos en t.me/programacion para más contenido tech diario.
0 Comentarios