So che questa è la risposta a trentaquattro domande a questa domanda, ma penso che ne valga la pena, quindi ecco qui. Questa è una soluzione solo CSS con le seguenti proprietà:
- Non vi è alcun ritardo all'inizio e la transizione non si interrompe in anticipo. In entrambe le direzioni (espansione e compressione), se si specifica una durata di transizione di 300 ms nel CSS, la transizione richiede 300 ms, punto.
- Sta spostando l'altezza effettiva (a differenza
transform: scaleY(0)
), quindi fa la cosa giusta se ci sono contenuti dopo l'elemento pieghevole.
- Mentre (come in altre soluzioni) ci sono numeri magici (come "scegli una lunghezza superiore a quella che sarà mai la tua scatola"), non è fatale se il tuo presupposto finisce per essere sbagliato. La transizione potrebbe non sembrare sorprendente in quel caso, ma prima e dopo la transizione, questo non è un problema: nello stato espanso (
height: auto
), l'intero contenuto ha sempre l'altezza corretta (a differenza, ad esempio, se scegli una max-height
che risulta essere troppo basso). E nello stato collassato, l'altezza è zero come dovrebbe.
dimostrazione
Ecco una demo con tre elementi pieghevoli, tutti di diverse altezze, che usano tutti lo stesso CSS. Potresti voler fare clic su "pagina intera" dopo aver fatto clic su "esegui snippet". Si noti che JavaScript attiva o disattiva solo la collapsed
classe CSS, non sono previste misurazioni. (È possibile eseguire questa dimostrazione esatta senza JavaScript utilizzando una casella di controllo o :target
). Inoltre, la parte del CSS responsabile della transizione è piuttosto breve e l'HTML richiede solo un singolo elemento wrapper aggiuntivo.
$(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>
Come funziona?
Ci sono infatti due transizioni coinvolte nel realizzare questo. Uno di questi passa margin-bottom
dallo 0px (nello stato espanso) allo -2000px
stato compresso (simile a questa risposta ). Il 2000 qui è il primo numero magico, si basa sul presupposto che la tua casella non sarà più alta di questa (2000 pixel sembrano una scelta ragionevole).
L'uso della margin-bottom
sola transizione da solo ha due problemi:
- Se in realtà hai una scatola più alta di 2000 pixel, allora
margin-bottom: -2000px
non nasconderai tutto: ci saranno oggetti visibili anche nel caso compresso. Questa è una correzione minore che faremo più tardi.
- Se la casella effettiva è, diciamo, alta 1000 pixel e la tua transizione è lunga 300 ms, allora la transizione visibile è già finita dopo circa 150 ms (o, nella direzione opposta, inizia 150ms in ritardo).
Risolvendo questo secondo problema è dove entra in gioco la seconda transizione, e questa transizione mira concettualmente all'altezza minima del wrapper ("concettualmente" perché non stiamo effettivamente usando la min-height
proprietà per questo; ne parleremo più avanti).
Ecco un'animazione che mostra come combinare la transizione del margine inferiore con la transizione dell'altezza minima, entrambe di uguale durata, ci dia una transizione combinata da tutta altezza a zero altezza che ha la stessa durata.
La barra di sinistra mostra come il margine inferiore negativo spinge il fondo verso l'alto, riducendo l'altezza visibile. La barra centrale mostra come l'altezza minima garantisce che nel caso del collasso, la transizione non finisca presto e, nel caso in espansione, la transizione non inizi in ritardo. La barra di destra mostra come la combinazione delle due fa sì che la scatola passi dalla piena altezza a zero altezza nel tempo corretto.
Per la mia demo ho optato per 50px come valore di altezza minima superiore. Questo è il secondo numero magico e dovrebbe essere inferiore all'altezza della scatola. Anche 50px sembrano ragionevoli; sembra improbabile che tu voglia molto spesso rendere pieghevole un elemento che non sia nemmeno alto 50 pixel in primo luogo.
Come puoi vedere nell'animazione, la transizione risultante è continua, ma non differenziabile: nel momento in cui l'altezza minima è uguale all'altezza completa regolata dal margine inferiore, si verifica un improvviso cambio di velocità. Ciò è molto evidente nell'animazione perché utilizza una funzione di temporizzazione lineare per entrambe le transizioni e perché l'intera transizione è molto lenta. Nel caso reale (la mia demo in alto), la transizione richiede solo 300 ms e la transizione del margine inferiore non è lineare. Ho giocato con molte diverse funzioni di temporizzazione per entrambe le transizioni e quelle con cui ho finito mi sembrava che funzionassero meglio per la più ampia varietà di casi.
Rimangono due problemi da risolvere:
- il punto dall'alto, in cui le caselle di altezza superiore a 2000 pixel non sono completamente nascoste nello stato compresso,
- e il problema inverso, in cui nel caso non nascosto, le caselle di altezza inferiore a 50 pixel sono troppo alte anche quando la transizione non è in esecuzione, poiché l'altezza minima le mantiene a 50 pixel.
Risolviamo il primo problema dando l'elemento contenitore a max-height: 0
nel caso compresso, con una 0s 0.3s
transizione. Ciò significa che non è in realtà una transizione, ma max-height
viene applicata con un ritardo; si applica solo al termine della transizione. Perché ciò funzioni correttamente, dobbiamo anche scegliere un valore numerico max-height
per lo stato opposto, non compresso. Ma a differenza del caso 2000px, in cui la selezione di un numero troppo grande influisce sulla qualità della transizione, in questo caso non importa. Quindi possiamo solo scegliere un numero così alto che sappiamo che nessuna altezza potrà mai avvicinarsi a questo. Ho scelto un milione di pixel. Se ritieni di dover supportare contenuti di altezza superiore a un milione di pixel, 1) mi dispiace e 2) aggiungi solo un paio di zeri.
Il secondo problema è il motivo per cui non stiamo effettivamente utilizzando min-height
per la transizione di altezza minima. Invece, c'è uno ::after
pseudo-elemento nel contenitore con un height
che passa da 50px a zero. Questo ha lo stesso effetto di a min-height
: Non consente al contenitore di ridursi al di sotto dell'altezza attualmente presente nello pseudo-elemento. Ma poiché stiamo usando height
, ora non min-height
possiamo usare max-height
(ancora una volta applicato con un ritardo) per impostare l'altezza effettiva dello pseudo-elemento a zero una volta terminata la transizione, assicurando che almeno al di fuori della transizione, anche i piccoli elementi abbiano il altezza corretta. Perché min-height
è più forte di max-height
, questo non funzionerebbe se usassimo il contenitore min-height
anziché gli pseudo-elementiheight
. Proprio come max-height
nel paragrafo precedente, max-height
anche questo ha bisogno di un valore per l'estremità opposta della transizione. Ma in questo caso possiamo solo scegliere il 50px.
Testato su Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (ad eccezione di un problema di layout flexbox con la mia demo che non ho disturbato il debug) e Safari (Mac, iOS ). Parlando di flexbox, dovrebbe essere possibile farlo funzionare senza usare alcun flexbox; in effetti penso che potresti far funzionare quasi tutto in IE7 - tranne per il fatto che non avrai transizioni CSS, rendendolo un esercizio piuttosto inutile.
height:auto/max-height
soluzione funzionerà solo se l'area in espansione è maggiore di quella cheheight
si desidera limitare. Se hai unmax-height
di300px
, ma un menu a discesa della casella combinata, che può restituire50px
, quindimax-height
non ti aiuterà,50px
è variabile a seconda del numero di elementi, puoi arrivare a una situazione impossibile in cui non posso ripararlo perché non loheight
è risolto,height:auto
era la soluzione, ma non posso usare le transizioni con questo.