⏱️ 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
  1. Qué es clip-path: shape()
  2. Sintaxis básica
  3. Primer ejemplo: un diamante
  4. Segundo ejemplo: una gota de agua
  5. El poder real: custom properties
  6. Ejemplo avanzado: pieza de puzzle con CSS
    1. Estructura base
    2. Agregando pestañas con curvas
    3. Parametrizando con custom properties
  7. Armando el puzzle completo
    1. Leyendo los data attributes desde CSS
  8. Mostrando una imagen dentro del puzzle
  9. Progressive enhancement con @supports
  10. Usos creativos más allá del puzzle
  11. Compatibilidad de navegadores
  12. Preguntas frecuentes
    1. ¿Cuál es la diferencia entre shape() y polygon()?
    2. ¿Puedo animar shape()?
    3. ¿shape() reemplaza a SVG?
  13. 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 @supports para 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 punto
  • curve 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 horizontal
  • vline to Y — línea vertical
  • close — 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-size con el signo opuesto al --right-size de la pieza de la izquierda. Si una tiene -15% (pestaña), la vecina necesita 15% (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 -1 para 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, usa style.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/3 ajusta por el espacio extra de las pestañas, y el -5/3 en 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

Navegadorclip-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

Síguenos en t.me/programacion para más contenido tech diario.


0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.