@position-try y el reposicionamiento cuando no cabe
Las tres formas de declarar posiciones alternativas, cómo elige el motor entre ellas, y position-try-order para quedarse con la que mejor entra.
Un menú anclado debajo de su botón funciona perfectamente hasta que el botón está al final de la pantalla. Entonces el menú se sale, y la única forma de arreglarlo era medir el espacio disponible en JavaScript y decidir si voltearlo. El sistema de posiciones alternativas mueve también esa decisión al motor: declaras una lista de configuraciones aceptables y el navegador prueba en orden hasta que una cabe.
- Declarar alternativas con las palabras clave
flip-*. - Escribir una lista de
position-areacomo alternativas. - Definir configuraciones completas con
@position-try. - Elegir la alternativa que más espacio deja con
position-try-order.
Cómo funciona el mecanismo
position-try-fallbacks acepta una lista separada por comas. El motor coloca el elemento con las declaraciones que tú escribiste. Si el resultado desborda su rectángulo de visualización —el viewport, o el contenedor de scroll correspondiente— descarta esa posición y prueba la primera alternativa de la lista. Y así sucesivamente. Si ninguna cabe, se queda con la posición original.
La comprobación de desbordamiento es del elemento entero, incluidos sus márgenes, contra el rectángulo que la especificación llama el bloque contenedor de posicionamiento reducido por position-try y el inset-area de recorte. En la práctica: contra la ventana.
Las tres formas de declarar alternativas
Forma 1: palabras clave de volteo. Las más cortas y las que resuelven el caso frecuente.
.menu {
position: absolute;
position-anchor: --boton;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
flip-block refleja la posición en el eje de bloque: lo que estaba debajo pasa a estar encima. flip-inline hace lo propio en el eje en línea. flip-start intercambia los dos ejes, que sirve para pasar de un menú vertical a uno lateral. Y se pueden combinar en una misma entrada, separadas por espacios, para voltear en los dos ejes a la vez.
La lista del ejemplo cubre las cuatro esquinas: abajo-derecha, arriba-derecha, abajo-izquierda, arriba-izquierda. Es la configuración por defecto de cualquier menú contextual bien hecho, en cuatro palabras.
Forma 2: lista de position-area. Cuando quieres controlar exactamente qué alternativas se prueban y en qué orden, en vez de dejarlo en manos de las reflexiones.
.tooltip {
position-area: block-start center;
position-try-fallbacks: block-end center, inline-end center, inline-start center;
}
Se lee muy bien: intenta arriba, si no cabe abajo, si no a la derecha, si no a la izquierda. Es el comportamiento clásico de un tooltip.
Forma 3: @position-try con nombre. Cuando la alternativa no es solo un cambio de posición, sino un cambio de configuración: otro margen, otro tamaño máximo, otra alineación.
@position-try --lateral {
position-area: inline-end center;
margin: 0 0 0 .5rem;
max-inline-size: 14rem;
}
@position-try --abajo-estrecho {
position-area: block-end span-inline-end;
margin-block-start: .5rem;
max-inline-size: anchor-size(width);
}
.menu {
position-area: block-end center;
position-try-fallbacks: --lateral, --abajo-estrecho;
}
Dentro de un bloque @position-try solo se aceptan propiedades relacionadas con la colocación: los desplazamientos y sus variantes lógicas, los márgenes, las propiedades de tamaño y de tamaño mínimo y máximo, position-anchor, position-area, y las propiedades de autoalineación justify-self, align-self y place-self. Cualquier otra declaración se ignora, así que no puedes cambiar el color ni el padding desde ahí.
Las tres formas se pueden mezclar en la misma lista: position-try-fallbacks: flip-block, --lateral, inline-end center es válido.
flowchart TB
A[El motor coloca el elemento con la posicion declarada] --> B{Desborda el rectangulo de visualizacion}
B -->|No| C[Se queda con esa posicion]
B -->|Si| D[Prueba la primera alternativa de la lista]
D --> E{Desborda}
E -->|No| F[Se queda con esa]
E -->|Si| G[Prueba la siguiente]
G --> H{Quedan alternativas}
H -->|Si| E
H -->|No| I{Hay position-try-order}
I -->|No| J[Vuelve a la posicion original aunque desborde]
I -->|Si| K[Elige la que deja mas espacio en el eje pedido]
style A fill:#89b4fa,color:#11111b
style C fill:#a6e3a1,color:#11111b
style F fill:#a6e3a1,color:#11111b
style J fill:#f38ba8,color:#11111b
style K fill:#f9e2af,color:#11111bposition-try-order: quedarse con la mejor, no con la primera
El algoritmo por defecto es de primera coincidencia: se queda con la primera alternativa que no desborde. Eso está bien cuando tienes una preferencia clara, pero es malo cuando ninguna cabe del todo y quieres la menos mala.
position-try-order cambia el criterio. Sus valores son normal —el comportamiento por defecto—, most-width, most-height, most-block-size y most-inline-size.
.menu {
position-area: block-end center;
position-try-fallbacks: flip-block;
position-try-order: most-block-size;
max-block-size: 20rem;
overflow-y: auto;
}
Con most-block-size, el motor evalúa todas las alternativas y se queda con la que deja más altura disponible, aunque la primera hubiera cabido. Combinado con un max-block-size y un scroll propio, produce el comportamiento que quieres de un menú largo cerca del borde: se abre hacia el lado donde hay más sitio y se recorta con scroll en vez de salirse.
Existe además el atajo position-try, que acepta el orden y la lista en una sola declaración:
.menu { position-try: most-block-size flip-block; }
El detalle de las transiciones
Cuando el motor cambia de una posición a otra, el cambio es instantáneo. No hay interpolación entre configuraciones, y no la habrá: interpolar entre dos conjuntos de declaraciones distintas no está definido.
Si quieres suavizar el momento, la vía es animar propiedades que no participen en la colocación —opacidad, un translate pequeño— y dejar que el salto de posición ocurra bajo esa animación. Y conviene respetar la preferencia del usuario:
.menu {
transition: opacity .12s, translate .12s;
}
@media (prefers-reduced-motion: reduce) {
.menu { transition-duration: 1ms; }
}
La parte de las bibliotecas de posicionamiento que más código tiene no es colocar el elemento, es decidir si cabe. Y ahí hay una trampa que cualquiera que haya escrito una conoce: la comprobación cambia lo que comprueba. Colocas el tooltip abajo, mides, ves que desborda, lo colocas arriba, mides otra vez… y resulta que arriba desborda también, porque al moverlo cambió el rectángulo de scroll de la página. Vuelves a abajo. Bucle infinito. Las implementaciones reales lo cortan con un contador de iteraciones y aceptando la última posición probada, lo que significa que el resultado depende del orden en que se evaluó y no siempre es el óptimo. La especificación evita esto por construcción con una regla que conviene conocer: durante la evaluación de las alternativas, el elemento anclado no contribuye al desbordamiento de scroll del documento. Es decir, probar una posición no puede cambiar el rectángulo contra el que se prueba. Con esa garantía, el conjunto de alternativas viables está definido antes de empezar a probar, el algoritmo termina siempre, y position-try-order puede evaluarlas todas y elegir la mejor sin riesgo de oscilar. Es un buen ejemplo de que llevar algo a la plataforma no consiste en reimplementar lo que hacía la biblioteca, sino en añadir el invariante que la biblioteca no podía imponer desde fuera.
- Coloca un menú debajo de un botón situado al final de la página y comprueba que se sale.
- Añade
position-try-fallbacks: flip-blocky verifica que ahora se abre hacia arriba. - Amplía la lista a las cuatro esquinas y prueba el botón en las cuatro esquinas de la ventana.
- Define un
@position-trycon nombre que además cambie elmax-inline-sizey comprueba que se aplica. - Compara el comportamiento con
position-try-order: normaly conmost-block-sizeen un menú largo cerca del borde inferior.