Je sais que c'est la trentaine de réponse à cette question, mais je pense que ça vaut le coup, alors voilà. Il s'agit d'une solution CSS uniquement avec les propriétés suivantes:
- Il n'y a pas de retard au début et la transition ne s'arrête pas tôt. Dans les deux sens (expansion et réduction), si vous spécifiez une durée de transition de 300 ms dans votre CSS, la transition prend 300 ms, point.
- C'est la transition de la hauteur réelle (contrairement
transform: scaleY(0)
), donc cela fait la bonne chose s'il y a du contenu après l'élément pliable.
- Bien que (comme dans d'autres solutions) il existe des nombres magiques (comme "choisir une longueur plus élevée que votre boîte ne le sera jamais"), ce n'est pas fatal si votre hypothèse finit par être fausse. La transition peut ne pas sembler incroyable dans ce cas, mais avant et après la transition, ce n'est pas un problème: dans l'état expand (
height: auto
), tout le contenu a toujours la bonne hauteur (contrairement par exemple si vous choisissez unmax-height
qui se révèle être trop bas). Et à l'état replié, la hauteur est nulle comme il se doit.
Démo
Voici une démo avec trois éléments pliables, tous de hauteurs différentes, qui utilisent tous le même CSS. Vous voudrez peut-être cliquer sur "pleine page" après avoir cliqué sur "exécuter l'extrait". Notez que le JavaScript ne fait que basculer la collapsed
classe CSS, aucune mesure n'est impliquée. (Vous pouvez faire cette démonstration exacte sans aucun JavaScript en utilisant une case à cocher ou :target
). Notez également que la partie du CSS responsable de la transition est assez courte et que le HTML ne nécessite qu'un seul élément wrapper supplémentaire.
$(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>
Comment ça marche?
Il y a en fait deux transitions impliquées pour que cela se produise. L'un d'eux transite margin-bottom
de 0px (à l'état développé) à -2000px
l'état réduit (similaire à cette réponse ). Le 2000 ici est le premier chiffre magique, il est basé sur l'hypothèse que votre boîte ne sera pas plus élevée que cela (2000 pixels semble être un choix raisonnable).
L'utilisation margin-bottom
seule de la transition pose deux problèmes:
- Si vous avez une boîte supérieure à 2000 pixels, alors
margin-bottom: -2000px
tout ne sera pas caché - il y aura des choses visibles même dans le cas replié. Il s'agit d'une correction mineure que nous ferons plus tard.
- Si la zone réelle est, disons, haute de 1000 pixels et que votre transition est longue de 300 ms, alors la transition visible est déjà terminée après environ 150 ms (ou, dans le sens opposé, commence 150 ms en retard).
La correction de ce deuxième problème est là où la deuxième transition entre en jeu, et cette transition cible conceptuellement la hauteur minimale de l'encapsuleur ("conceptuellement" parce que nous n'utilisons pas réellement la min-height
propriété pour cela; plus à ce sujet plus tard).
Voici une animation qui montre comment la combinaison de la transition de la marge inférieure avec la transition de la hauteur minimale, toutes deux de durée égale, nous donne une transition combinée de la pleine hauteur à la hauteur zéro qui a la même durée.
La barre de gauche montre comment la marge inférieure négative pousse le bas vers le haut, réduisant la hauteur visible. La barre du milieu montre comment la hauteur minimale garantit que dans le cas de la fermeture, la transition ne se termine pas tôt et dans le cas en expansion, la transition ne commence pas tard. La barre de droite montre comment la combinaison des deux fait passer la boîte de la pleine hauteur à la hauteur zéro dans le bon laps de temps.
Pour ma démo, j'ai choisi 50px comme valeur de hauteur minimale supérieure. Il s'agit du deuxième nombre magique, et il devrait être inférieur à la hauteur de la boîte. 50px semble également raisonnable; il semble peu probable que vous souhaitiez très souvent rendre un élément pliable qui n'a même pas 50 pixels de haut en premier lieu.
Comme vous pouvez le voir dans l'animation, la transition résultante est continue, mais elle n'est pas différenciable - au moment où la hauteur minimale est égale à la hauteur totale ajustée par la marge inférieure, il y a un changement soudain de vitesse. Ceci est très visible dans l'animation car elle utilise une fonction de synchronisation linéaire pour les deux transitions et parce que la transition entière est très lente. Dans le cas réel (ma démo en haut), la transition ne prend que 300 ms et la transition de la marge inférieure n'est pas linéaire. J'ai joué avec de nombreuses fonctions de synchronisation différentes pour les deux transitions, et celles avec lesquelles je me suis retrouvé me semblaient fonctionner le mieux pour la plus grande variété de cas.
Deux problèmes restent à résoudre:
- le point d'en haut, où les boîtes de plus de 2000 pixels de hauteur ne sont pas complètement masquées à l'état replié,
- et le problème inverse, où dans le cas non caché, les boîtes de moins de 50 pixels de hauteur sont trop hautes même lorsque la transition n'est pas en cours, car la hauteur minimale les maintient à 50 pixels.
Nous résolvons le premier problème en donnant à l'élément conteneur un max-height: 0
dans le cas réduit, avec une 0s 0.3s
transition. Cela signifie que ce n'est pas vraiment une transition, mais que max-height
c'est appliqué avec un retard; il ne s'applique qu'une fois la transition terminée. Pour que cela fonctionne correctement, nous devons également choisir une valeur numérique max-height
pour l'état opposé, non effondré. Mais contrairement au cas 2000px, où le choix d'un nombre trop important affecte la qualité de la transition, dans ce cas, cela n'a vraiment pas d'importance. Nous pouvons donc simplement choisir un nombre si élevé que nous savons qu'aucune hauteur ne pourra jamais s'en approcher. J'ai choisi un million de pixels. Si vous pensez que vous devrez peut-être prendre en charge un contenu d'une hauteur de plus d'un million de pixels, alors 1) je suis désolé et 2) ajoutez simplement quelques zéros.
Le deuxième problème est la raison pour laquelle nous n'utilisons pas réellement min-height
pour la transition de hauteur minimale. Au lieu de cela, il y a un ::after
pseudo-élément dans le conteneur avec un height
qui passe de 50px à zéro. Cela a le même effet qu'un min-height
: il ne laissera pas le conteneur rétrécir en dessous de la hauteur actuelle du pseudo-élément. Mais parce que nous utilisons height
, non min-height
, nous pouvons maintenant utiliser max-height
(une fois de plus appliqué avec un retard) pour définir la hauteur réelle du pseudo-élément à zéro une fois la transition terminée, garantissant qu'au moins en dehors de la transition, même les petits éléments ont le hauteur correcte. Parce que min-height
est plus fort que max-height
, cela ne fonctionnerait pas si nous utilisions le conteneur au min-height
lieu du pseudo-élémentheight
. Tout commemax-height
dans le paragraphe précédent, cela max-height
nécessite également une valeur pour l'extrémité opposée de la transition. Mais dans ce cas, nous pouvons simplement choisir le 50px.
Testé dans Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (à l'exception d'un problème de mise en page flexbox avec ma démo que je n'ai pas pris la peine de déboguer) et Safari (Mac, iOS ). En parlant de flexbox, il devrait être possible de faire ce travail sans utiliser de flexbox; en fait, je pense que vous pourriez faire fonctionner presque tout dans IE7 - à l'exception du fait que vous n'aurez pas de transitions CSS, ce qui en fait un exercice plutôt inutile.
height:auto/max-height
solution ne fonctionnera que si vous agrandissez la zone est supérieure à celle queheight
vous souhaitez restreindre. Si vous avez unmax-height
de300px
, mais une liste déroulante de zone de liste déroulante, qui peut retourner50px
, puismax-height
ne vous aidera pas,50px
est variable en fonction du nombre d'éléments, vous pouvez arriver à une situation impossible où je ne peux pas le réparer parce que leheight
n'est pas fixe,height:auto
était la solution, mais je ne peux pas utiliser de transitions avec cela.