Sé que esta es la respuesta de treinta y tantos a esta pregunta, pero creo que vale la pena, así que aquí va. Esta es una solución solo CSS con las siguientes propiedades:
- No hay demora al principio, y la transición no se detiene temprano. En ambas direcciones (expandir y contraer), si especifica una duración de transición de 300 ms en su CSS, la transición dura 300 ms, punto.
- Está
transform: scaleY(0)
haciendo la transición de la altura real (a diferencia de ), por lo que hace lo correcto si hay contenido después del elemento plegable.
- Mientras que (al igual que en otras soluciones) no son números mágicos (como "escoger una longitud que es mayor que su caja es cada vez va a ser"), no es fatal si sus extremos asunción siendo mal. La transición puede no parecer sorprendente en ese caso, pero antes y después de la transición, esto no es un problema: en el estado expandido (
height: auto
), todo el contenido siempre tiene la altura correcta (a diferencia, por ejemplo, si elige un max-height
que resulta ser demasiado baja). Y en el estado colapsado, la altura es cero como debería.
Manifestación
Aquí hay una demostración con tres elementos plegables, todos de diferentes alturas, que usan el mismo CSS. Es posible que desee hacer clic en "página completa" después de hacer clic en "ejecutar fragmento". Tenga en cuenta que JavaScript solo alterna la collapsed
clase CSS, no hay mediciones involucradas. (Puede hacer esta demostración exacta sin ningún tipo de JavaScript utilizando una casilla de verificación o :target
). También tenga en cuenta que la parte del CSS que es responsable de la transición es bastante corta, y el HTML solo requiere un único elemento contenedor adicional.
$(function () {
$(".toggler").click(function () {
$(this).next().toggleClass("collapsed");
$(this).toggleClass("toggled"); // this just rotates the expander arrow
});
});
.collapsible-wrapper {
display: flex;
overflow: hidden;
}
.collapsible-wrapper:after {
content: '';
height: 50px;
transition: height 0.3s linear, max-height 0s 0.3s linear;
max-height: 0px;
}
.collapsible {
transition: margin-bottom 0.3s cubic-bezier(0, 0, 0, 1);
margin-bottom: 0;
max-height: 1000000px;
}
.collapsible-wrapper.collapsed > .collapsible {
margin-bottom: -2000px;
transition: margin-bottom 0.3s cubic-bezier(1, 0, 1, 1),
visibility 0s 0.3s, max-height 0s 0.3s;
visibility: hidden;
max-height: 0;
}
.collapsible-wrapper.collapsed:after
{
height: 0;
transition: height 0.3s linear;
max-height: 50px;
}
/* END of the collapsible implementation; the stuff below
is just styling for this demo */
#container {
display: flex;
align-items: flex-start;
max-width: 1000px;
margin: 0 auto;
}
.menu {
border: 1px solid #ccc;
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
margin: 20px;
}
.menu-item {
display: block;
background: linear-gradient(to bottom, #fff 0%,#eee 100%);
margin: 0;
padding: 1em;
line-height: 1.3;
}
.collapsible .menu-item {
border-left: 2px solid #888;
border-right: 2px solid #888;
background: linear-gradient(to bottom, #eee 0%,#ddd 100%);
}
.menu-item.toggler {
background: linear-gradient(to bottom, #aaa 0%,#888 100%);
color: white;
cursor: pointer;
}
.menu-item.toggler:before {
content: '';
display: block;
border-left: 8px solid white;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
width: 0;
height: 0;
float: right;
transition: transform 0.3s ease-out;
}
.menu-item.toggler.toggled:before {
transform: rotate(90deg);
}
body { font-family: sans-serif; font-size: 14px; }
*, *:after {
box-sizing: border-box;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="container">
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
</div>
¿Como funciona?
De hecho, hay dos transiciones involucradas en hacer que esto suceda. Uno de ellos hace la transición margin-bottom
de 0px (en el estado expandido) al -2000px
estado colapsado (similar a esta respuesta ). El 2000 aquí es el primer número mágico, se basa en el supuesto de que su caja no será más alta que esta (2000 píxeles parece una opción razonable).
Usar la margin-bottom
transición solo por sí solo tiene dos problemas:
- Si realmente tiene un cuadro que tiene más de 2000 píxeles, entonces
margin-bottom: -2000px
no lo ocultará todo, habrá cosas visibles incluso en el caso colapsado. Esta es una solución menor que haremos más adelante.
- Si el cuadro real tiene, digamos, 1000 píxeles de alto, y su transición es de 300 ms de largo, entonces la transición visible ya ha terminado después de aproximadamente 150 ms (o, en la dirección opuesta, comienza 150 ms tarde).
La solución de este segundo problema es donde entra la segunda transición, y esta transición se dirige conceptualmente a la altura mínima del contenedor ("conceptualmente" porque en realidad no estamos usando la min-height
propiedad para esto; más sobre eso más adelante).
Aquí hay una animación que muestra cómo la combinación de la transición del margen inferior con la transición de altura mínima, ambas de igual duración, nos proporciona una transición combinada de altura completa a altura cero que tiene la misma duración.
La barra izquierda muestra cómo el margen inferior negativo empuja el fondo hacia arriba, reduciendo la altura visible. La barra del medio muestra cómo la altura mínima asegura que en el caso de colapso, la transición no termine temprano, y en el caso de expansión, la transición no comienza tarde. La barra derecha muestra cómo la combinación de los dos hace que la caja pase de la altura completa a la altura cero en la cantidad de tiempo correcta.
Para mi demo, me decidí por 50px como el valor de altura mínima superior. Este es el segundo número mágico, y debería ser más bajo que la altura de la caja. 50 px parece razonable también; parece poco probable que a menudo quiera hacer un elemento plegable que no tenga ni 50 píxeles de altura en primer lugar.
Como puede ver en la animación, la transición resultante es continua, pero no es diferenciable: en el momento en que la altura mínima es igual a la altura completa ajustada por el margen inferior, hay un cambio repentino en la velocidad. Esto es muy notable en la animación porque usa una función de temporización lineal para ambas transiciones, y porque toda la transición es muy lenta. En el caso real (mi demo en la parte superior), la transición solo toma 300 ms, y la transición del margen inferior no es lineal. He jugado con muchas funciones de temporización diferentes para ambas transiciones, y las que terminé sentían que funcionaban mejor para la más amplia variedad de casos.
Quedan dos problemas por solucionar:
- el punto de arriba, donde los cuadros de más de 2000 píxeles de altura no están completamente ocultos en el estado colapsado,
- y el problema inverso, donde en el caso no oculto, los cuadros de menos de 50 píxeles de altura son demasiado altos incluso cuando la transición no se está ejecutando, porque la altura mínima los mantiene en 50 píxeles.
Resolvemos el primer problema dando al elemento contenedor a max-height: 0
en el caso colapsado, con una 0s 0.3s
transición. Esto significa que no es realmente una transición, sino que max-height
se aplica con retraso; solo se aplica una vez que finaliza la transición. Para que esto funcione correctamente, también debemos elegir un valor numérico max-height
para el estado opuesto, no colapsado. Pero a diferencia del caso de 2000px, donde elegir un número demasiado grande afecta la calidad de la transición, en este caso, realmente no importa. Entonces, podemos elegir un número que sea tan alto que sepamos que ninguna altura se acercará a esto. Elegí un millón de píxeles. Si cree que puede necesitar contenido de una altura de más de un millón de píxeles, entonces 1) lo siento, y 2) simplemente agregue un par de ceros.
El segundo problema es la razón por la que en realidad no estamos utilizando min-height
la transición de altura mínima. En cambio, hay un ::after
pseudo-elemento en el contenedor con una height
transición de 50 px a cero. Esto tiene el mismo efecto que un min-height
: No permitirá que el contenedor se encoja por debajo de la altura que tenga el pseudo-elemento actualmente. Pero debido a que estamos usando height
, no min-height
, ahora podemos usar max-height
(una vez más aplicado con un retraso) para establecer la altura real del pseudo-elemento a cero una vez que finaliza la transición, asegurando que al menos fuera de la transición, incluso los elementos pequeños tengan el altura correcta Porque min-height
es más fuerte que max-height
, esto no funcionaría si usáramos los contenedores en min-height
lugar de los pseudoelementosheight
. Al igual que max-height
en el párrafo anterior, esto max-height
también necesita un valor para el extremo opuesto de la transición. Pero en este caso, podemos elegir el 50px.
Probado en Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (excepto por un problema de diseño de flexbox con mi demo que no me molestó en depurar) y Safari (Mac, iOS ) Hablando de flexbox, debería ser posible hacer que esto funcione sin usar ningún flexbox; de hecho, creo que podría hacer que casi todo funcione en IE7, excepto el hecho de que no tendrá transiciones CSS, lo que lo convierte en un ejercicio bastante inútil.
height:auto/max-height
solución solo funcionará si su área de expansión es mayor que laheight
que desea restringir. Si tiene unmax-height
de300px
, pero un menú desplegable de cuadro combinado, que puede regresar50px
,max-height
que no lo ayudará,50px
es variable dependiendo de la cantidad de elementos, puede llegar a una situación imposible donde no puedo solucionarlo porqueheight
no es arreglado,height:auto
era la solución, pero no puedo usar transiciones con esto.