Sei que essa é a trigésima terceira resposta a essa pergunta, mas acho que vale a pena, então aqui vai. Esta é uma solução somente CSS com as seguintes propriedades:
- Não há atraso no início, e a transição não para mais cedo. Nas duas direções (expansão e recolhimento), se você especificar uma duração de transição de 300ms no seu CSS, a transição levará 300ms, ponto final.
- Ele está fazendo a transição da altura real (diferente
transform: scaleY(0)
), por isso faz a coisa certa se houver conteúdo após o elemento recolhível.
- Enquanto (como em outras soluções), há são números mágicos (como "pegar um comprimento que é maior do que sua caixa está sempre vai ser"), não é fatal se suas extremidades suposição até estar errado. A transição pode não parecer incrível nesse caso, mas antes e depois da transição, isso não é um problema: No estado expandido (
height: auto
), todo o conteúdo sempre tem a altura correta (diferente de, por exemplo, se você escolher uma max-height
que seja muito baixo). E no estado recolhido, a altura é zero como deveria.
Demo
Aqui está uma demonstração com três elementos recolhíveis, com diferentes alturas, que usam o mesmo CSS. Você pode clicar em "página inteira" após clicar em "executar trecho". Observe que o JavaScript apenas alterna a collapsed
classe CSS, não há medição envolvida. (Você pode fazer esta demonstração exata sem qualquer JavaScript usando uma caixa de seleção ou :target
). Observe também que a parte do CSS responsável pela transição é bastante curta e o HTML requer apenas um único elemento adicional do wrapper.
$(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 fato, existem duas transições envolvidas para que isso aconteça. Um deles faz a transição margin-bottom
de 0px (no estado expandido) para -2000px
no estado recolhido (semelhante a esta resposta ). O 2000 aqui é o primeiro número mágico, é baseado na suposição de que sua caixa não será maior que isso (2000 pixels parece uma escolha razoável).
Usando o margin-bottom
transição sozinha por si só tem dois problemas:
- Se você realmente tiver uma caixa com mais de 2000 pixels, uma
margin-bottom: -2000px
não ocultará tudo - haverá coisas visíveis mesmo no caso recolhido. Esta é uma pequena correção que faremos mais tarde.
- Se a caixa real tiver, digamos, 1000 pixels de altura e sua transição tiver 300 ms, a transição visível já estará concluída após cerca de 150 ms (ou, na direção oposta, inicia 150 ms tarde).
A correção desse segundo problema é onde entra a segunda transição, e essa transição visa conceitualmente a altura mínima do wrapper ("conceitualmente" porque não estamos realmente usando a min-height
propriedade para isso; mais sobre isso posteriormente).
Aqui está uma animação que mostra como a combinação da transição da margem inferior com a transição mínima da altura, ambas de igual duração, nos fornece uma transição combinada da altura total para a altura zero que tem a mesma duração.
A barra esquerda mostra como a margem inferior negativa empurra a parte inferior para cima, reduzindo a altura visível. A barra do meio mostra como a altura mínima garante que, no caso de colapso, a transição não termine mais cedo e, no caso de expansão, a transição não comece tarde. A barra direita mostra como a combinação dos dois faz com que a caixa faça a transição da altura total para a altura zero na quantidade correta de tempo.
Para minha demonstração, estabeleci 50px como o valor mínimo de altura superior. Este é o segundo número mágico e deve ser menor do que a altura da caixa jamais seria. 50px parece razoável também; parece improvável que você queira muitas vezes tornar um elemento dobrável que não tem nem 50 pixels de altura.
Como você pode ver na animação, a transição resultante é contínua, mas não diferenciável - no momento em que a altura mínima é igual à altura total ajustada pela margem inferior, ocorre uma mudança repentina na velocidade. Isso é muito perceptível na animação porque usa uma função de tempo linear para ambas as transições e porque toda a transição é muito lenta. No caso real (minha demonstração no topo), a transição leva apenas 300ms e a transição da margem inferior não é linear. Eu brinquei com muitas funções de tempo diferentes para ambas as transições, e as que eu acabei achando que funcionaram melhor na mais ampla variedade de casos.
Restam dois problemas para corrigir:
- o ponto acima, onde caixas com mais de 2000 pixels de altura não estão completamente ocultas no estado recolhido,
- e o problema inverso, onde, no caso não oculto, caixas com menos de 50 pixels de altura são muito altas, mesmo quando a transição não está em execução, porque a altura mínima as mantém em 50 pixels.
Resolvemos o primeiro problema, atribuindo ao elemento contêiner a max-height: 0
no caso recolhido, com uma 0s 0.3s
transição. Isso significa que não é realmente uma transição, mas max-height
é aplicada com um atraso; só se aplica quando a transição termina. Para que isso funcione corretamente, também precisamos escolher um numérico max-height
para o estado oposto, sem recolhimento . Mas, diferentemente do caso de 2000 px, onde escolher um número muito grande afeta a qualidade da transição, nesse caso, isso realmente não importa. Assim, podemos simplesmente escolher um número que é tão alto que sabemos que nenhuma altura chegará perto disso. Eu escolhi um milhão de pixels. Se você acha que precisa oferecer suporte a conteúdo com mais de um milhão de pixels de altura, 1) desculpe-me e 2) apenas adicione alguns zeros.
O segundo problema é a razão pela qual não estamos realmente usando min-height
a transição de altura mínima. Em vez disso, há um ::after
pseudoelemento no contêiner height
que transita de 50px para zero. Isso tem o mesmo efeito que min-height
: Não permitirá que o contêiner encolha abaixo da altura que o pseudoelemento tenha atualmente. Mas como estamos usando height
, não min-height
, agora podemos usar max-height
(mais uma vez aplicado com atraso) para definir a altura real do pseudo-elemento como zero quando a transição terminar, garantindo que, pelo menos fora da transição, até mesmo pequenos elementos tenham o altura correta. Por min-height
ser mais forte que max-height
isso não funcionaria se usássemos o contêiner em min-height
vez do pseudo-elementoheight
. Assim como omax-height
no parágrafo anterior, isso max-height
também precisa de um valor para o extremo oposto da transição. Mas, neste caso, podemos simplesmente escolher os 50px.
Testado no Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (exceto por um problema de layout da caixa flexível na minha demonstração que não incomodava a depuração) e Safari (Mac, iOS ) Falando em flexbox, deve ser possível fazer isso funcionar sem usar flexbox; na verdade, acho que você poderia fazer quase tudo funcionar no IE7 - exceto pelo fato de não ter transições CSS, tornando-o um exercício inútil.
height:auto/max-height
solução só funcionará se você estiver expandindo a área maior do que aheight
que deseja restringir. Se você tem um menu suspensomax-height
de300px
, mas uma caixa de combinação, que pode retornar50px
emax-height
não ajudá-lo,50px
é variável dependendo do número de elementos, você pode chegar a uma situação impossível em que eu não posso consertá-lo porqueheight
não é fixo,height:auto
foi a solução, mas não posso usar transições com isso.