rect: el rectángulo y sus radios
La geometría de rect, el comportamiento auto de rx y ry, el recorte silencioso a la mitad de la caja, y por qué un rect redondeado no es un border-radius.
El rectángulo es la forma que todo el mundo cree conocer y la que más sorpresas guarda. Tiene cuatro atributos obvios y dos que se comportan de una manera que no está escrita en ningún tutorial: rx y ry se cubren mutuamente cuando falta uno, y ambos se recortan en silencio si te pasas. Entender ese recorte es la diferencia entre una píldora que sale como la dibujaste y una que se queda a medias sin decirte por qué.
- Escribir un
rectcon su geometría completa y saber contra qué se resuelve cada porcentaje. - Predecir el radio resultante cuando solo se declara
rx, solory, o valores mayores que media caja. - Enumerar las tres capacidades de
border-radiusque unrectde SVG no tiene, y cómo se suplen. - Decidir cuándo un
rectdeja de ser la herramienta y hay que bajar apath.
La geometría mínima
Un rect se define con cuatro longitudes: x e y marcan la esquina superior izquierda en el sistema de coordenadas del usuario, y width y height la extensión. Los cuatro tienen valor inicial 0, así que un rect sin atributos existe en el DOM pero ocupa un punto y no pinta nada.
<svg viewBox="0 0 200 100" width="400">
<rect x="10" y="10" width="180" height="80" fill="#89b4fa" />
</svg>
Lo primero que hay que interiorizar es que ese x="10" no es una coordenada de pantalla. Es una coordenada del sistema que establece el viewBox, y el navegador la escala al tamaño real del elemento. Si el svg mide 400 px de ancho y el viewBox declara 200 unidades, cada unidad vale dos píxeles CSS. Escribir 10 significa «una décima parte del ancho del lienzo», no «diez píxeles».
Un width o un height negativo es un error de valor: el elemento no se renderiza. Un valor de cero tampoco pinta nada, pero es un valor válido y no rompe el documento; la distinción importa cuando generas la forma desde datos, porque un cero legítimo (una barra con valor cero) y un negativo accidental (una barra con un dominio mal invertido) producen el mismo vacío visual por razones distintas.
Los porcentajes se resuelven contra el viewport SVG actual, y no todos contra lo mismo. Una longitud del eje horizontal (x, width, rx) usa el ancho del viewport. Una del eje vertical (y, height, ry) usa el alto. Es un detalle que se paga caro en rx: un rx="50%" sobre un rectángulo estrecho dentro de un viewport ancho da un radio enorme que luego se recorta, y el resultado no se parece en nada a lo que esperabas.
rx, ry y la palabra auto
Aquí empieza lo interesante. Ambos atributos tienen valor inicial auto, y auto significa «usa el valor del otro». Las consecuencias, en orden:
- Si declaras solo
rx, entoncesryvale lo mismo y las esquinas son circulares. - Si declaras solo
ry,rxcopia su valor. Mismo resultado. - Si declaras los dos, las esquinas son arcos elípticos con esos semiejes.
- Si no declaras ninguno, ambos valen cero y el rectángulo es recto.
<svg viewBox="0 0 320 80" width="640">
<rect x="10" y="10" width="90" height="60" rx="12" fill="#a6e3a1" />
<rect x="115" y="10" width="90" height="60" rx="12" ry="30" fill="#f9e2af" />
<rect x="220" y="10" width="90" height="60" rx="999" fill="#cba6f7" />
</svg>
El tercero es el caso que hay que memorizar. rx="999" no produce un error ni un rectángulo deforme: el navegador recorta el radio a la mitad de la dimensión correspondiente. rx se limita a width / 2 y ry a height / 2. Con 90 de ancho y 60 de alto, ese 999 acaba valiendo 45 en horizontal y 30 en vertical, y sale la píldora perfecta. Es el truco estándar para una cápsula sin calcular nada, y funciona igual si el rectángulo cambia de tamaño.
Ese recorte es también la causa del bug contrario. Si animas width de un rect con rx fijo, mientras el ancho sea mayor que el doble del radio no pasa nada raro; en cuanto baja de ahí, el radio empieza a encogerse solo y la forma se deforma de un modo que no habías pedido. No es un fallo del motor: es el recorte haciendo su trabajo.
Mucha gente cree que el recorte se aplica al par rx/ry como conjunto, preservando la proporción. No: cada uno se recorta contra su propia dimensión, de forma independiente. Un rect de 200x40 con rx="100" ry="100" acaba con rx=100 (cabe, porque la mitad del ancho es 100) y ry=20 (recortado a la mitad del alto). El resultado son cuatro cuartos de elipse muy achatados que se tocan en el centro de los lados largos, no una píldora. Si lo que querías era una cápsula, declara solo uno de los dos y deja que auto haga el resto, o usa un valor absurdamente grande en el que sí declaras: entonces el otro copia el valor antes del recorte, y ambos se recortan cada uno a lo suyo.
Lo que un rect no puede hacer
border-radius en CSS es mucho más expresivo, y conviene tener claras las tres diferencias porque son las tres razones por las que acabarás en path.
La primera: no hay control por esquina. border-radius acepta cuatro valores y redondea cada vértice de forma distinta. rect aplica el mismo par rx/ry a las cuatro esquinas, sin excepción. Una tarjeta con las esquinas superiores redondeadas y las inferiores rectas no se puede hacer con un rect.
La segunda: no hay resolución del solapamiento proporcional. Cuando en CSS la suma de dos radios adyacentes supera el lado, el navegador reduce todos los radios por un mismo factor para mantener la proporción. En SVG cada eje se recorta a la mitad y punto.
La tercera: no existe la esquina asimétrica dentro de un mismo vértice. En CSS puedes dar horizontal y vertical distintos por esquina con la barra. Aquí solo hay un rx y un ry globales.
Cuando necesitas cualquiera de las tres, la salida es un path. Y como el path de un rectángulo redondeado es mecánico, merece la pena tener la función escrita una vez:
// Rectangulo con radio independiente por esquina. Devuelve el atributo d.
function rectRedondeado(x, y, w, h, { ti = 0, td = 0, bd = 0, bi = 0 } = {}) {
const max = Math.min(w, h) / 2;
const a = Math.min(ti, max), b = Math.min(td, max);
const c = Math.min(bd, max), d = Math.min(bi, max);
return [
`M ${x + a} ${y}`,
`H ${x + w - b}`, `A ${b} ${b} 0 0 1 ${x + w} ${y + b}`,
`V ${y + h - c}`, `A ${c} ${c} 0 0 1 ${x + w - c} ${y + h}`,
`H ${x + d}`, `A ${d} ${d} 0 0 1 ${x} ${y + h - d}`,
`V ${y + a}`, `A ${a} ${a} 0 0 1 ${x + a} ${y}`,
'Z',
].join(' ');
}
// Tarjeta con las dos esquinas de arriba redondeadas
rectRedondeado(0, 0, 200, 120, { ti: 16, td: 16 });
Los comandos que aparecen ahí (M, H, V, A, Z) son el mini-lenguaje del atributo d, y tienen su propio nivel: el elemento path los desmonta uno a uno, incluida la firma completa del arco elíptico que aquí se usa a ciegas.
Cuándo el rect sigue ganando
Con todas esas limitaciones, la tentación es convertir todo a path y olvidarse. Sería un error, y por tres motivos concretos.
El primero es que x, y, width y height son propiedades de geometría animables y consultables. Puedes leer rect.width.baseVal.value desde JavaScript, puedes transicionarlas desde CSS en los motores que exponen las propiedades de geometría, y puedes cambiarlas con un setAttribute sin recomponer una cadena. El atributo d de un path es una cadena opaca: para mover un lado tienes que regenerarla entera.
El segundo es la legibilidad del documento. Un SVG generado por servidor donde las barras de un gráfico son rect se lee, se depura y se testea. El mismo gráfico con paths es un muro de números.
El tercero es que el selector CSS rect existe. Si conviertes las formas a path como parte de un paso de optimización, cualquier regla que apunte a rect deja de aplicar en silencio; volveremos sobre ello en SVGO y los plugins que rompen cosas.
La regla práctica: usa rect mientras la forma sea un rectángulo con radio uniforme o nulo, que es el noventa por ciento de los rectángulos que dibujarás. Baja a path solo cuando aparezca la primera esquina distinta de las demás.
Dibuja una barra de progreso con extremos capsulares donde el relleno interior también sea capsular y nunca sobresalga del carril. Hazlo primero con dos rect y rx grande, y comprueba qué ocurre visualmente cuando el progreso baja del 5 por ciento. Después arréglalo sin path: piensa en qué pasa si el rect interior tiene el mismo rx que el exterior y anchura muy pequeña, y decide si el arreglo correcto es recortar el interior con un clipPath con la forma del carril.