Chat Mobile Fullscreen di Bawah Header Fixed 66px
Wrapper mobile fullscreen di bawah header fixed 66px pakai mt-66px plus calc 100dvh-66px, transcript tanpa cap 45vh, dan header ringkas ala aplikasi pesan.
Di ponsel, kotak chat saya awalnya cuma nangkring di tengah halaman. Header di atas sudah fixed, tapi wadah chat di bawahnya tetap dihitung dari atas viewport. Hasilnya sebagian kepotong di balik header, sebagian lagi ngambang dengan ruang kosong yang nggak jelas di bawahnya. Terkaan awal saya waktu itu: mungkin overflow di wrapper yang salah. Ternyata bukan.
Akarnya lebih dasar. Elemen dengan position: fixed keluar dari alur dokumen dan tidak menyisakan ruang [1], posisinya mengacu ke containing block awal yang di media visual adalah viewport [1]. Jadi konten di bawahnya memang tidak otomatis tahu berapa tinggi header. Kalau wadah chat tetap saya set h-screen atau 100vh mentah, ukurannya tetap satu layar penuh termasuk area yang sudah tertutup header. Di layar kecil, composer yang harusnya nempel di bawah malah melayang di tengah.
Untuk ngecek konteks viewport-nya, saya buka lagi halaman konsep viewport di MDN. Di sana dijelaskan viewport adalah fitur user agent yang jadi containing block awal untuk media kontinu [2]. Ini yang bikin fixed selalu nempel ke viewport, bukan ke induknya. Bedanya tipis tapi ngaruh: layout viewport bisa beda dari visual viewport saat keyboard muncul atau saat pinch-zoom.
Offset manual plus calc untuk sisa layar
Solusinya jadi dua langkah yang harus berpasangan: kasih jarak atas setinggi header, lalu isi sisa layar dengan hitungan. Di commit 16d5d0c header di proyek ini setinggi 66px (isinya judul pendek Tanya AI plus pill Beta di kanan), jadi wrapper mobile saya ubah dari min-h-[85vh] mengambang menjadi mt-[66px] h-[calc(100dvh-66px)] dengan overflow-hidden. Angka 66px ini contoh di proyek saya, bukan aturan universal — sesuaikan dengan tinggi header masing-masing.
Pola hitungnya datang dari fungsi calc() yang memang untuk menghitung nilai properti CSS [3]. Lalu untuk unit viewport-nya, saya pakai dvh bukan vh biasa. Tailwind mendokumentasikan h-dvh sebagai utility yang tingginya mengikuti viewport dinamis yang berubah saat UI browser mengembang atau mengempis [4], dan kelas arbitrari seperti h-[calc(100dvh-66px)] didukung lewat sintaks h-[<value>] [4]. Di ponsel, dvh lebih akurat daripada vh karena address bar yang muncul hilang ikut dihitung.
<div class="order-1 mt-[66px] flex h-[calc(100dvh-66px)] flex-col overflow-hidden lg:hidden">
<ChatMobileThread turns={turns} ... />
</div>
Header di dalamnya saya ringkas sekalian: buang tombol back dan baris greeting panjang, ganti dengan judul halaman plus pill Beta di kanan. Lebih mirip aplikasi pesan, dan hemat tinggi.
Transcript tanpa batas kaku
Di iterasi sebelumnya transcript dibatasi max-h-[45vh]. Waktu wrapper sudah dihitung dinamis, batas itu malah bikin rantai scroll putus. Pengguna harus menggulir transcript dulu, mentok, baru menggulir wrapper. Secara rasa, kayak scroll dua kali untuk satu daftar.
Perbaikannya: hapus max-h-[45vh], biarkan transcript jadi flex-1 dengan overflow-y-auto di dalam wrapper flex-col yang tingginya sudah pasti. Dengan begitu pembagian ruangnya otomatis: header setinggi kontennya, transcript ambil sisa, composer di bawah tetap di tempatnya. Composer yang tadinya melayang jadi benar-benar nempel di bawah layar seperti WhatsApp.
<div class="flex min-h-0 flex-1 flex-col">
<div class="flex shrink-0 items-center gap-2 border-b px-3 py-2">
<p>Tanya AI</p><span>Beta</span>
</div>
<div class="min-h-0 flex-1 overflow-y-auto px-3 py-3">
{/* bubbles */}
</div>
</div>
Karena riwayat di mobile tidak lagi ditampilkan sebagai kolom yang bisa dibuka tutup, state historyOpen dan handler back yang mengaturnya ikut saya hapus dari AiChatWorkspace. Satu state hilang, satu prop onBack hilang, dan impor ikon yang tidak terpakai ikut bersih. Saya pilih menghapus daripada menyembunyikan — kalau fiturnya memang tidak ada di mobile, kodenya juga tidak perlu tinggal sebagai beban.
Kalau header di proyek lain tingginya beda, rumusnya tetap sama: ukur tinggi header yang fixed, kasih margin-top yang sama, lalu height: calc(100dvh - header). Untuk kasus header h-14 (56px), pasangannya jadi mt-14 dan h-[calc(100dvh-3.5rem)]. Di sisi mobile lain yang masih butuh pola serupa, saya pernah juga memisah kerangka mobile dengan logika jawaban tetap satu sumber lewat komponen chat terpisah di mobile supaya perubahan tampilan tidak menggandakan logika.
Sumber
- [1] MDN: position, diakses 2026-09-08
- [2] MDN: Viewport concepts, diakses 2026-09-08
- [3] MDN: calc(), diakses 2026-09-08
- [4] Tailwind CSS: height, diakses 2026-09-08