Di atas kertas, repeat(7, 1fr) berarti tujuh kolom sama lebar. Di kalender bulanan yang saya bangun ulang untuk aplikasi web milik klien, deklarasi yang sama menghasilkan empat kolom selebar 271 sampai 275 piksel dan tiga kolom selebar 26 sampai 27 piksel.
Aplikasinya React 18 yang dibangun dengan Create React App lewat react-scripts 5.0.1, dan gayanya ditulis dengan styled-components 5. Kalender versi barunya saya kirim sebagai cabang fitur pada 8 September 2026, malam yang sama dengan permintaan klien. Cabang itu tercatat bisa digabung bersih ke cabang pengembangan klien, dan build produksinya lolos. Tampilan bulannya menggambar 42 sel, termasuk hari-hari dari bulan sebelum dan sesudahnya, dan setiap sel menampilkan paling banyak tiga chip acara. Sisanya diringkas menjadi tombol "+N more".
Laporan masalahnya datang dari pengembang backend di pihak klien, terhadap cabang yang sudah di-push: sebagian sel kalender besar, sisanya kecil. Temuan itu benar.
Tujuh kolom, dua ukuran
Diukur sebelum perbaikan, lebar ketujuh kolomnya adalah 272, 275, 275, 271, 27, 27, dan 26 piksel. Kolom terlebar lebih dari sepuluh kali lipat lebar kolom tersempit, karena 275 dibagi 26 sekitar 10,6.
Tingginya juga tidak rata. Grid 42 sel dengan tujuh kolom berarti enam baris minggu, dan baris-baris itu terbelah ke dua ukuran, 88 dan 95 piksel. Menurut pesan commit perbaikannya, sel hanya memegang min-height, sehingga minggu yang berisi acara berdiri lebih tinggi daripada minggu yang kosong, 95 lawan 88.
Ada satu hal lagi yang bisa dibaca dari angka kolomnya. Jumlah ketujuh kolom sebelum perbaikan adalah 272 + 275 + 275 + 271 + 27 + 27 + 26 = 1.173 piksel. Sesudah perbaikan, pada data yang sama, ketujuhnya masing-masing 115 piksel, jadi 7 × 115 = 805 piksel. Empat kolom lebar itu tidak sekadar merebut jatah tiga kolom sempit, karena deretan tujuh kolom itu secara keseluruhan 368 piksel lebih lebar. Lebar wadah grid-nya tidak tercatat, jadi selisih itu saya biarkan sebagai hitungan, bukan klaim tentang seberapa jauh grid-nya meluber.
Catatan saya merumuskan ketidakrataan ini sebagai dua sebab terpisah yang berakar sama, yaitu item grid atau item flex yang menolak menyusut. Yang grid ada di trek kolomnya. Yang flex ada di dalam chip acara.
1fr punya batas bawah, dan batasnya auto
Sebelum perbaikan, grid bulan dan header harinya sama-sama memakai repeat(7, 1fr), dan sel bulannya diberi min-height: 88px. Di dalam sel, chip acara berupa tombol flex dengan span terpisah untuk judulnya. Berikut versi yang disederhanakan ke CSS biasa, dengan nama kelas yang saya netralkan karena aslinya ditulis sebagai styled-components:
/* disederhanakan dari styled-components, nama kelas dinetralkan */
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
/* header hari memakai deklarasi kolom yang sama */
.weekdays {
grid-template-columns: repeat(7, 1fr);
}
.day {
min-height: 88px;
}
.chip {
display: flex;
align-items: center;
gap: 5px;
font-size: 10.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.chip .label {
overflow: hidden;
text-overflow: ellipsis;
}Yang mudah terlewat, 1fr tidak berdiri sendiri. Di luar minmax(), nilai fleksibel seperti 1fr membawa batas bawah otomatis, sehingga repeat(7, 1fr) sebenarnya adalah repeat(7, minmax(auto, 1fr)). Batas bawah auto berarti kolom tidak boleh lebih sempit daripada ukuran minimum item di dalamnya. Item grid bawaannya memakai min-width: auto, dan selama overflow-nya visible, ukuran minimum itu adalah lebar min-content isinya, yaitu lebar terkecil yang bisa dicapai konten tanpa meluber. Untuk teks, itu berarti selebar potongan terpanjang yang tidak bisa dipatah.
Potongan seperti itu ada di data kalendernya. Seperti dicatat salah satu pesan commit-nya, rapat yang judulnya tidak pernah diketik penyelenggaranya datang bernama tautan join-nya. Beberapa judul berupa tautan rapat video itu panjangnya kira-kira 36 karakter tanpa kesempatan patah baris, di dalam chip yang memang memasang white-space: nowrap. Satu judul seperti itu cukup untuk melebarkan kolomnya, dan karena ketujuh kolom berbagi ruang yang sama, kolom-kolom lain yang terdesak.
Ketika jumlah batas bawah ketujuh kolom sudah melampaui ruang yang tersedia, 1fr tidak lagi punya sisa ruang untuk dibagi, dan setiap kolom berhenti di batas bawahnya sendiri walaupun grid-nya jadi lebih lebar daripada wadahnya. Komentar di header hari sesudah perbaikan merangkum mekanismenya dengan lebih pendek: 1fr polos tetap menolak menyusut di bawah benda terlebar di dalam kolomnya, jadi satu judul rapat yang panjang dan tidak bisa dipatah menyeret kolomnya melebar dan menekan enam kolom lainnya.
Ellipsis di kotak yang salah
Bagian flex-nya ada di dalam chip. Chip acara adalah tombol display: flex yang kotaknya sendiri memasang white-space: nowrap, overflow: hidden, dan text-overflow: ellipsis, sebelum maupun sesudah perbaikan. Deklarasi terakhir itu tidak berbuat apa-apa di sana. text-overflow hanya berlaku pada block container, sedangkan kotak flex bukan block container, karena isinya ditata sebagai item flex. Mock di paket layar milik klien membuat kesalahan yang sama dengan menaruh text-overflow: ellipsis langsung di kotak flex chip-nya, dan kesimpulan catatan saya untuk mock itu berlaku juga di sini: judulnya butuh elemen sendiri.
Di build saya judulnya memang sudah punya elemen sendiri, sebuah span yang sebelum perbaikan hanya berisi overflow: hidden dan text-overflow: ellipsis. Catatan saya waktu itu menimpakan masalahnya pada span tersebut. Span itu memasang ellipsis tanpa min-width: 0, dan karena anak flex tidak mau menyusut di bawah kontennya tanpa deklarasi itu, chip melebar alih-alih terpotong. Catatan yang sama menyebut satu deklarasi yang hilang itulah yang membuat seluruh kolom menggembung.
Bagian ini tidak sekuat kedengarannya. Aturan bahwa item flex tidak menyusut di bawah kontennya berlaku untuk item yang overflow-nya visible. Spesifikasi flexbox memberi item yang menjadi scroll container ukuran minimum otomatis nol, dan overflow: hidden sudah menjadikan sebuah item scroll container. Span tadi memasang overflow: hidden sejak sebelum perbaikan. Pengukuran yang saya punya juga hanya membandingkan keadaan sebelum dan sesudah seluruh perbaikan, jadi tidak ada angka yang bisa memisahkan andil span ini dari andil perubahan di level grid.
Batas bawah nol di setiap tingkat
Perbaikannya dikerjakan di cabang tersendiri, dua commit di atas cabang fitur. Commit dimensinya hanya mengubah satu berkas gaya kalender, 20 baris ditambah dan 2 dihapus, dan tercatat selesai 9 September 2026. Di bawah ini hanya baris yang relevan, dan isi dua komentar di dalamnya saya kutip apa adanya dari kode:
.days-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
/* Every week the same height, so the grid stays a grid. Without this
a week holding three events stands taller than an empty one. */
grid-auto-rows: 1fr;
}
.weekdays {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.day {
/* A grid item defaults to min-width:auto, which is its content width.
Without this the cell cannot shrink and neither can its column. */
min-width: 0;
overflow: hidden;
}
.chip {
width: 100%;
min-width: 0;
max-width: 100%;
}
.chip .label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}Grid bulan dan header hari sama-sama pindah ke repeat(7, minmax(0, 1fr)). Header harus ikut, karena kalau deklarasi kolomnya berbeda dari grid, label harinya tidak lagi sejajar dengan kolom di bawahnya. Sel diberi min-width: 0 dan overflow: hidden. Kotak chip mendapat min-width: 0 dan max-width: 100% di samping width: 100% yang sudah ada. Span judulnya mendapat min-width: 0 dan white-space: nowrap, dengan komentar di kode yang menyebut min-width: 0 itulah yang membuat ellipsis bekerja.
Sebagian deklarasi ini saling menutupi. Menurut spesifikasi grid, ukuran minimum otomatis sebuah item hanya diambil dari kontennya kalau item itu bukan scroll container dan berada di trek yang batas bawahnya auto. overflow: hidden di sel menggugurkan syarat pertama, dan minmax(0, 1fr) menggugurkan syarat kedua. Semuanya masuk dalam satu commit, jadi hasil ukurnya berlaku untuk gabungan semuanya: pada data yang sama, ketujuh kolom kini masing-masing 115 piksel.
Baris diurus oleh grid-auto-rows: 1fr. Pada grid yang tingginya mengikuti isi, semua baris 1fr mendapat tinggi yang sama, yaitu setinggi baris yang isinya paling tinggi. Hasil ukurnya cocok dengan itu: baris yang tadinya 88 dan 95 piksel kini rata 95.
Paket mock layar milik klien ternyata punya min-height yang sama, dan menurut catatan saya cacat yang sama ikut tergambar di mock-nya sendiri. Paket itu juga mendefinisikan satu kelas yang tidak pernah dipakai mock-nya. Pesan commit perbaikan menyebut kelas itu sebagai jawaban yang sedang dicari paket tersebut: batasi isi sel, lalu sebutkan berapa yang tersembunyi. Bagian itu sudah ada di build saya sejak commit bangun ulang 8 September, sehari sebelum commit perbaikan grid, dalam bentuk tiga chip per sel dan tombol "+N more" untuk sisanya.
Begitu terpotong, judulnya cuma "https:..."
Begitu pemotongan bekerja, setiap chip yang judulnya tautan terbaca "https:..." dan tidak menyampaikan apa pun. Kolomnya sudah rata, tapi isinya kehilangan arti.
Keputusannya, judul yang berbentuk URL kini dicetak hanya bagian path-nya, judul yang bukan URL tidak disentuh, dan tooltip tetap memuat judul aslinya. Judul commit-nya merumuskan maksudnya untuk tautan join, print the meeting code when a title is just a join link. Fungsinya, dengan nama fungsi, komponen, dan kelas yang saya netralkan:
// nama fungsi, komponen, dan kelas dinetralkan
function shortTitle(name) {
const raw = (name || '').trim()
if (!raw) return 'Untitled'
if (!/^https?:\/\//i.test(raw)) return raw
try {
const u = new URL(raw)
const tail = u.pathname.replace(/^\/+|\/+$/g, '')
return tail || u.hostname
} catch {
return raw
}
}
// tooltip memegang judul asli, span memegang versi pendek
<button className="chip" title={`${formatTime(event)} ${event.name}`}>
<span className="time">{formatTime(event)}</span>
<span className="label">{shortTitle(event.name)}</span>
</button>Nama kosong menjadi "Untitled". Nama yang tidak diawali http:// atau https:// diteruskan apa adanya. Untuk URL, yang dikembalikan adalah path tanpa garis miring di kedua ujungnya, atau hostname kalau path-nya kosong, dan string yang gagal di-parse sebagai URL dikembalikan utuh. Atribut title di tombol chip tetap memakai nama asli, jadi tooltip masih memuat tautan lengkapnya, sementara span judul memakai versi pendeknya. Label ini masuk sebagai commit terpisah, tepat di atas commit perbaikan dimensi.
Dari cabang perbaikan ke cabang klien
Pada 9 September, cabang-cabang kalender, termasuk cabang perbaikan grid, digabung ke cabang kerja saya lalu dihapus. Lima hari kemudian, 14 September, isinya sampai ke cabang pengembangan klien lewat sebuah squash merge yang tercatat atas nama pengembang backend klien. Karena di-squash, SHA commit aslinya bukan leluhur cabang klien, sehingga yang perlu diperiksa adalah isi kodenya, bukan silsilah commit-nya. Isinya ada: di cabang pengembangan klien, grid bulan masih memakai repeat(7, minmax(0, 1fr)) dan grid-auto-rows: 1fr.
Batasnya juga perlu disebut. Pengukuran 115 dan 95 piksel tadi dilakukan di cabang perbaikan pada 9 September. Catatan saya tidak menyebut pengukuran ulang sesudah squash itu, dan tidak juga sesudah deploy ke produksi.
Batas bawah yang tidak pernah ditulis
Dua auto dalam kasus ini tidak pernah diketik siapa pun di berkas gayanya. Yang pertama tersembunyi di dalam 1fr, yang kedua ada di min-width bawaan item grid dan item flex. min-width: 0 pada item flex sudah jadi obat yang cukup dikenal. Yang membuat kasus ini lain adalah batas bawah yang sama ternyata juga duduk di trek grid, dan di sana dia melebarkan seluruh kolom, bukan cuma satu elemen.
Kalau kolom grid harus tetap sama lebar apa pun isinya, tulis batas bawahnya secara eksplisit dengan minmax(0, 1fr) dan biarkan isi sel yang menyesuaikan diri dengan kolomnya, bukan sebaliknya. Kalau teks di dalam kotak flex harus terpotong dengan ellipsis, taruh text-overflow di elemen anak, bukan di kotak flex-nya, pastikan anak itu boleh menyusut lewat min-width: 0 atau overflow selain visible, dan pastikan kotak di atasnya punya batas lebar yang nyata.
Sesudah teksnya terpotong, baca apa yang tersisa. Di kalender ini, pemotongan yang bekerja dengan benar menghasilkan deretan "https:..." yang tidak memberi tahu apa pun, dan jalan keluarnya ada di label yang dicetak, bukan di CSS.