Saya tahu ini adalah jawaban ketiga puluh untuk pertanyaan ini, tetapi saya pikir itu sepadan, jadi begini. Ini adalah solusi khusus CSS dengan properti berikut:
- Tidak ada penundaan di awal, dan transisi tidak berhenti lebih awal. Di kedua arah (meluas dan menciut), jika Anda menentukan durasi transisi 300 ms dalam CSS Anda, maka transisi membutuhkan 300 ms, titik.
- Ini mentransisikan ketinggian sebenarnya (tidak seperti
transform: scaleY(0)
), jadi itu melakukan hal yang benar jika ada konten setelah elemen dilipat.
- Sementara (seperti solusi lain) ada yang angka ajaib (seperti "memilih panjang yang lebih tinggi dari kotak Anda pernah akan menjadi"), itu tidak fatal jika asumsi Anda berakhir sampai menjadi salah. Transisi mungkin tidak terlihat luar biasa dalam kasus itu, tetapi sebelum dan setelah transisi, ini bukan masalah: Dalam keadaan diperluas (
height: auto
), seluruh konten selalu memiliki ketinggian yang benar (tidak seperti misalnya jika Anda memilih max-height
yang ternyata menjadi terlalu rendah). Dan dalam keadaan runtuh, ketinggiannya nol seperti seharusnya.
Demo
Berikut ini demo dengan tiga elemen yang dapat dilipat, semua ketinggian berbeda, yang semuanya menggunakan CSS yang sama. Anda mungkin ingin mengklik "halaman penuh" setelah mengklik "jalankan snippet". Perhatikan bahwa JavaScript hanya mengaktifkan collapsed
kelas CSS, tidak ada pengukuran yang terlibat. (Anda dapat melakukan demo ini tanpa JavaScript sama sekali dengan menggunakan kotak centang atau :target
). Perhatikan juga bahwa bagian dari CSS yang bertanggung jawab untuk transisi cukup singkat, dan HTML hanya membutuhkan satu elemen pembungkus tambahan.
$(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>
Bagaimana cara kerjanya?
Sebenarnya ada dua transisi yang terlibat dalam mewujudkan hal ini. Salah satunya transisi margin-bottom
dari 0px (dalam status diperluas) ke -2000px
dalam kondisi diciutkan (mirip dengan jawaban ini ). 2000 di sini adalah angka ajaib pertama, didasarkan pada asumsi bahwa kotak Anda tidak akan lebih tinggi dari ini (2000 piksel sepertinya pilihan yang masuk akal).
Menggunakan margin-bottom
transisi sendiri dengan sendirinya memiliki dua masalah:
- Jika Anda benar-benar memiliki kotak yang lebih tinggi dari 2000 piksel, maka
margin-bottom: -2000px
tidak akan menyembunyikan semuanya - akan ada barang yang terlihat bahkan dalam kasing yang runtuh. Ini adalah perbaikan kecil yang akan kami lakukan nanti.
- Jika kotak aktual, katakanlah, 1000 piksel tinggi, dan transisi Anda panjang 300 ms, maka transisi yang terlihat sudah berakhir setelah sekitar 150 ms (atau, dalam arah yang berlawanan, mulai terlambat 150 ms).
Memperbaiki masalah kedua ini adalah tempat transisi kedua masuk, dan transisi ini secara konseptual menargetkan tinggi minimum pembungkus ("secara konseptual" karena kita tidak benar-benar menggunakan min-height
properti untuk ini; lebih lanjut tentang itu nanti).
Berikut ini animasi yang menunjukkan bagaimana menggabungkan transisi margin bawah dengan transisi ketinggian minimum, keduanya memiliki durasi yang sama, memberi kita transisi gabungan dari tinggi penuh ke tinggi nol yang memiliki durasi yang sama.
Bilah kiri menunjukkan bagaimana margin bawah negatif mendorong bagian bawah ke atas, mengurangi ketinggian yang terlihat. Bilah tengah menunjukkan bagaimana ketinggian minimum memastikan bahwa dalam kasus runtuh, transisi tidak berakhir lebih awal, dan dalam kasus ekspansi, transisi tidak dimulai terlambat. Bilah kanan menunjukkan bagaimana kombinasi keduanya menyebabkan kotak untuk transisi dari ketinggian penuh ke ketinggian nol dalam jumlah waktu yang benar.
Untuk demo saya, saya telah menetapkan 50px sebagai nilai tinggi minimum atas. Ini adalah angka ajaib kedua, dan itu harus lebih rendah dari ketinggian kotak yang pernah ada. 50px tampaknya masuk akal juga; tampaknya tidak mungkin Anda sering ingin membuat elemen yang dapat dilipat yang bahkan tidak setinggi 50 piksel.
Seperti yang Anda lihat dalam animasi, transisi yang dihasilkan adalah kontinu, tetapi tidak dapat dibedakan - pada saat tinggi minimum sama dengan tinggi penuh yang disesuaikan dengan margin bawah, ada perubahan kecepatan yang tiba-tiba. Ini sangat nyata dalam animasi karena menggunakan fungsi pewaktuan linear untuk kedua transisi, dan karena keseluruhan transisi sangat lambat. Dalam kasus aktual (demo saya di atas), transisi hanya membutuhkan 300 ms, dan transisi margin bawah tidak linier. Saya telah bermain-main dengan banyak fungsi pengaturan waktu yang berbeda untuk kedua transisi, dan yang akhirnya saya rasakan sepertinya mereka bekerja paling baik untuk berbagai kasus terluas.
Masih ada dua masalah yang harus diperbaiki:
- titik dari atas, di mana kotak-kotak dengan ketinggian lebih dari 2000 piksel tidak sepenuhnya tersembunyi dalam keadaan runtuh,
- dan masalah sebaliknya, di mana dalam kasus non-tersembunyi, kotak dengan tinggi kurang dari 50 piksel terlalu tinggi bahkan ketika transisi tidak berjalan, karena ketinggian minimum membuat mereka di 50 piksel.
Kami memecahkan masalah pertama dengan memberikan elemen wadah a max-height: 0
dalam kasus runtuh, dengan 0s 0.3s
transisi. Ini berarti bahwa itu bukan transisi, tetapi max-height
diterapkan dengan penundaan; itu hanya berlaku setelah transisi selesai. Agar ini berfungsi dengan benar, kita juga perlu memilih numerik max-height
untuk keadaan sebaliknya, yang tidak diciutkan. Tetapi tidak seperti dalam kasus 2000px, di mana memilih nomor yang terlalu besar mempengaruhi kualitas transisi, dalam kasus ini, itu benar-benar tidak masalah. Jadi kita bisa memilih angka yang begitu tinggi sehingga kita tahu bahwa tidak ada ketinggian yang akan mendekati ini. Saya memilih satu juta piksel. Jika Anda merasa Anda perlu mendukung konten dengan ketinggian lebih dari satu juta piksel, maka 1) Saya minta maaf, dan 2) tambahkan saja beberapa nol.
Masalah kedua adalah alasan mengapa kita tidak benar-benar menggunakan min-height
untuk transisi ketinggian minimum. Sebaliknya, ada ::after
pseudo-elemen dalam wadah dengan height
transisi dari 50px ke nol. Ini memiliki efek yang sama dengan min-height
: Ini tidak akan membiarkan wadah menyusut di bawah tinggi apa pun yang dimiliki elemen pseudo saat ini. Tetapi karena kita menggunakan height
, bukan min-height
, kita sekarang dapat menggunakan max-height
(sekali lagi diterapkan dengan penundaan) untuk mengatur tinggi aktual pseudo-elemen menjadi nol setelah transisi selesai, memastikan bahwa setidaknya di luar transisi, bahkan elemen kecil memiliki ketinggian yang benar. Karena min-height
ini lebih kuat daripada max-height
, ini tidak akan bekerja jika kita menggunakan wadah ini min-height
bukan pseudo-elemenheight
. Sama seperti max-height
pada paragraf sebelumnya, ini max-height
juga membutuhkan nilai untuk kebalikan dari transisi. Tetapi dalam hal ini kita bisa memilih 50px.
Diuji di Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (kecuali untuk masalah tata letak flexbox dengan demo saya bahwa saya tidak mengganggu debugging), dan Safari (Mac, iOS ). Berbicara tentang flexbox, seharusnya dimungkinkan untuk membuatnya bekerja tanpa menggunakan flexbox apa pun; sebenarnya saya pikir Anda bisa membuat hampir semuanya berfungsi di IE7 - kecuali kenyataan bahwa Anda tidak akan memiliki transisi CSS, menjadikannya latihan yang agak tidak berguna.
height:auto/max-height
solusinya hanya akan berfungsi jika Anda memperluas area yang lebih besar daripada yangheight
ingin Anda batasi. Jika Anda memilikimax-height
dari300px
, tapi combo box dropdown, yang dapat kembali50px
, makamax-height
tidak akan membantu Anda,50px
adalah variabel tergantung pada jumlah elemen, Anda dapat tiba ke situasi sulit di mana aku tidak bisa memperbaikinya karenaheight
tidak diperbaiki,height:auto
adalah solusinya, tetapi saya tidak dapat menggunakan transisi dengan ini.