ray() y la caja de referencia
Coordenadas polares en CSS: los cuatro parámetros de ray(), qué mide cada palabra clave de tamaño, y cómo el valor de coord-box cambia el trazado sin tocar la forma.
ray() es la parte del módulo que no tiene equivalente en ningún otro sitio de CSS: una semirrecta definida por un ángulo, una longitud y un origen, sobre la que un elemento se desplaza. Dicho así suena a poco. Dicho de otra forma, es posicionamiento en coordenadas polares, y CSS nunca había tenido eso. Los patrones que resuelve —menús radiales, explosiones desde un punto, elementos que salen despedidos en direcciones distintas con la misma regla— se escribían antes con un translate() distinto por elemento calculado con un seno y un coseno en la hoja de estilos o, peor, en JavaScript.
- Escribir un
ray()con sus cuatro parámetros y saber cuáles son opcionales. - Distinguir qué mide cada palabra clave de tamaño y cuándo
sidesno es lo mismo queclosest-side. - Predecir el efecto de
containsobre el recorrido real de un elemento con tamaño. - Cambiar el trazado sin cambiar la forma, eligiendo la caja de referencia adecuada.
Cuatro parámetros, uno obligatorio
La gramática es ray( <angle> && <ray-size>? && contain? && [at <position>]? ). El doble ampersand significa que el orden entre ellos es libre: ray(45deg closest-side contain) y ray(contain closest-side 45deg) son el mismo valor. Solo el ángulo es obligatorio.
El ángulo se mide como en conic-gradient() y como en la brújula: 0deg apunta hacia arriba y los grados crecen en sentido horario. Esto choca de frente con la convención matemática de medir desde el eje x positivo y hacia la izquierda, así que si estás traduciendo fórmulas de un artículo de trigonometría tendrás que rotar y reflejar. Merece la pena interiorizar la de CSS: 90deg es hacia la derecha, 180deg hacia abajo, 270deg hacia la izquierda.
El origen es el at <position>, con la misma gramática que background-position. Si lo omites, el origen es el punto que indique offset-position, y si tampoco hay offset-position, el centro del elemento en su posición de layout. Ese encadenamiento es cómodo: puedes tener una regla común con el ángulo y variar solo el origen por elemento.
El tamaño es lo que determina qué significa offset-distance: 100%. Sin él, el valor por defecto es closest-side.
| Palabra clave | Longitud del rayo |
|---|---|
closest-side |
Del origen al lado más cercano del bloque contenedor |
farthest-side |
Del origen al lado más lejano |
closest-corner |
Del origen a la esquina más cercana |
farthest-corner |
Del origen a la esquina más lejana |
sides |
Del origen al punto donde el rayo cruza el borde del bloque contenedor |
Las cuatro primeras son idénticas a las de radial-gradient() y miden en línea recta hasta un lado o una esquina con independencia del ángulo del rayo. sides es la única que depende del ángulo: mide hasta donde el rayo, con su dirección concreta, sale de la caja. Es casi siempre la que quieres cuando el efecto es “vete hasta el borde”, y casi nunca la que la gente escribe porque no está en radial-gradient().
.contenedor { position: relative; width: 400px; height: 240px; }
.chispa {
position: absolute;
inset: 0;
margin: auto;
width: 8px; height: 8px;
background: #fab387;
/* Sale hacia arriba-derecha hasta tocar el borde del contenedor. */
offset-path: ray(45deg sides);
animation: salir 900ms ease-out forwards;
}
@keyframes salir {
from { offset-distance: 0%; opacity: 1; }
to { offset-distance: 100%; opacity: 0; }
}
En un contenedor de 400 por 240 con el origen en el centro, closest-side daría 120 píxeles —la mitad del alto, que es el lado más cercano— y la chispa se pararía a mitad de camino del borde derecho. sides da la distancia real hasta el punto de salida, que a 45 grados son unos 170 píxeles. Cambiar una palabra cambia el efecto por completo.
Si el origen del rayo cae fuera del bloque contenedor, sides y closest-corner dan longitud cero por definición, y con closest-side y farthest-side la especificación considera que los bordes de la caja se extienden hasta el infinito, con lo que sigues obteniendo una longitud finita. No es un caso raro: pasa en cuanto usas at con valores negativos o con porcentajes mayores de cien.
Qué hace contain de verdad
contain acorta el rayo lo justo para que la caja del elemento quepa entera dentro del bloque contenedor al llegar al final. Sin él, el rayo mide hasta el borde y el elemento, que tiene tamaño, sobresale la mitad de su ancho o de su alto.
La resta que aplica no es del ancho ni del alto sino de la mitad de la dimensión que corresponda según la orientación de la caja del elemento respecto al borde que toca. La consecuencia práctica es lo único que hay que recordar: con contain, offset-distance: 100% deja el elemento tangente por dentro; sin contain, lo deja centrado sobre el borde. Y si el elemento es más grande que la caja, la longitud resultante se recorta a cero en lugar de volverse negativa.
/* Sin contain: la mitad del cuadrado queda fuera al llegar al 100%. */
.a { offset-path: ray(90deg sides); }
/* Con contain: el cuadrado queda pegado por dentro al borde derecho. */
.b { offset-path: ray(90deg sides contain); }
El caso donde contain es imprescindible es el menú radial: quieres que los botones se desplieguen hasta el borde disponible sin que ninguno se corte, y quieres que eso siga siendo cierto cuando el contenedor cambie de tamaño. Con ray() y una variable por elemento el menú entero son seis líneas:
.radial button {
position: absolute;
inset: 0;
margin: auto;
offset-path: ray(calc(var(--i) * 60deg) closest-side contain);
offset-distance: 0%;
transition: offset-distance 320ms cubic-bezier(.2, .9, .3, 1.2);
}
.radial:hover button { offset-distance: 100%; }
Cada botón lleva su --i en el marcado y comparte todo lo demás. Seis botones, seis direcciones, una sola regla. Y como lo que se anima es offset-distance, la transición es interpolable sin trucos y el cubic-bezier con rebote se aplica al avance radial, no a dos ejes por separado.
flowchart LR
a[Origen del rayo] --> b[Angulo medido desde arriba en sentido horario]
b --> c{Palabra clave de tamano}
c -->|closest o farthest| d[Distancia a un lado o esquina sin mirar el angulo]
c -->|sides| e[Distancia al cruce real del rayo con el borde]
d --> f{contain}
e --> f
f -->|si| g[Se resta media caja del elemento]
f -->|no| h[El elemento queda centrado sobre el borde]
style d fill:#89b4fa,color:#11111b
style e fill:#a6e3a1,color:#11111b
style g fill:#a6e3a1,color:#11111b
style h fill:#f9e2af,color:#11111bLa caja de referencia cambia el trazado sin cambiar la forma
El segundo componente de offset-path es un <coord-box>, y puede ir con la forma o solo. Determina contra qué rectángulo se resuelven los porcentajes de la forma y dónde están sus bordes. Los valores son content-box, padding-box, border-box, fill-box, stroke-box y view-box. Si lo omites, el valor por defecto es border-box en contextos CSS; en contextos SVG, todos se comportan como view-box.
Esto convierte una decisión que parecía de detalle en una palanca real. Estos tres recorridos son formas distintas escritas con la misma función:
.a { offset-path: inset(0) border-box; }
.b { offset-path: inset(0) padding-box; }
.c { offset-path: inset(0) content-box; }
Con un contenedor que tenga borde y padding gruesos, el primero recorre el perímetro exterior, el segundo el interior del borde y el tercero el del padding. Sin <coord-box> habrías tenido que calcular a mano los inset() equivalentes, y recalcularlos cada vez que alguien tocase el padding.
fill-box y stroke-box solo tienen sentido sobre elementos SVG y se refieren a la caja del relleno y a la del trazo respectivamente; la segunda incluye el grosor del stroke, que es lo que quieres cuando un marcador debe seguir el borde exterior de una línea gruesa. view-box se refiere al sistema de coordenadas establecido por el viewBox del SVG más cercano, y es el que hace que un recorrido escrito en unidades de usuario siga siendo correcto cuando el SVG se escala.
Todas las demás formas de offset-path describen una geometría cerrada cuyo tamaño hay que fijar de algún modo: en píxeles, que no escala, o en porcentajes, que dependen de quién sea el bloque contenedor y por tanto se rompen al cambiar el position de un antepasado. ray() es la única que se define por una dirección y una regla de medida —sides, contain— en vez de por una magnitud. Esto la hace la opción robusta por defecto para cualquier efecto dirigido desde un punto: no hay ningún número que revisar cuando el layout cambia, porque no hay ningún número. Cuando dudes entre una polygon() con porcentajes y un ray(), y el efecto sea radial, el ray() va a sobrevivir a más refactorizaciones.
Reescribe el menú radial de arriba para que las direcciones no estén repartidas uniformemente sino concentradas en un abanico de 120 grados hacia arriba, y para que el radio de cada botón varíe con su índice. Pista: solo tienes que tocar la expresión dentro de ray() y añadir una segunda variable; ninguna otra regla cambia.