Skip to content

Kotak Deskripsi ala YouTube: Clamp Tiga Baris, Toggle Terukur

Adityo Guni Waluyo

Membangun kotak deskripsi video yang melipat tiga baris dengan toggle yang hanya muncul saat teks benar-benar terpotong.

Ringkasan

Deskripsi video kepanjangan bikin tombol suka sama komentar kejauhan, makanya teksnya di-clamp pakai line-clamp CSS cuma sampai tiga baris. Setelah halaman nge-mount, komponennya ngebandingin scrollHeight sama clientHeight buat nemuin teks yang kepotong, baru tombol "...selengkapnya" muncul kalau emang perlu. Testing di jsdom harus nge-mock offsetnya soalnya layout nggak dihitung, dan label "Deskripsi" dihapus biar tampilan makin beres.

Slug URLs: id /blog/kotak-desripsi-video-ala-youtube, en /blog/youtube-style-video-description-box. Dibawah draf.

---

# Kotak Deskripsi ala YouTube untuk Halaman Detail Video

Di halaman detail video KotaPortal, deskripsi dirender sebagai satu blok teks penuh di bawah judul. Video dengan deskripsi panjang mendorong tombol suka, bagikan, dan daftar komentar keluar dari layar pertama. Pengguna yang ingin berinteraksi harus menggulir melewati beberapa paragraf teks channel terlebih dahulu.

Solusi pertama yang dicoba adalah memotong teks di server pada 200 karakter. Pendekatan itu gagal dari tiga arah. Pemotongan jatuh di tengah kata tanpa peduli batas kata. Jumlah karakter tidak berhubungan langsung dengan tinggi piksel karena hasilnya bergantung pada lebar font dan lebar kotak, sehingga di layar sempit teks tetap panjang dan di layar lebar teks yang sebenarnya muat ikut terpotong. Teks juga harus dirender dua versi, pendek dan penuh, lalu ditukar di klien saat tombol diklik.

Koreksinya ada di dua lapisan yang berbeda. Pemotongan visual ditangani CSS melalui line-clamp [4], dan keputusan menampilkan tombol diambil setelah mount dengan mengukur overflow nyata [5]. Tidak ada asumsi jumlah karakter; yang diukur adalah tinggi piksel sesungguhnya.

Memotong di level CSS, mengukur setelah mount

Tailwind sudah menyediakan utilitas line-clamp-3, yang merupakan singkatan dari overflow: hidden, display: -webkit-box, -webkit-box-orient: vertical, dan -webkit-line-clamp: 3 [6]. Kombinasi -webkit- ini masih didukung penuh dan menjadi bentuk legacy yang dispesifikasikan [4]. Untuk membatalkan clamp saat teks diperluas, line-clamp-none mengembalikan properti tersebut [6].

Untuk mendeteksi apakah teks benar-benar terpotong, bandingkan scrollHeight dengan clientHeight setelah komponen terpasang. scrollHeight mengukur tinggi konten termasuk bagian yang tersembunyi oleh overflow; saat konten muat, nilainya sama dengan clientHeight [5]. Perbandingannya memakai threshold kecil, bukan kesetaraan eksak, karena nilai pembulatan subpiksel bisa membuat dua angka yang secara visual identik berbeda satu unit. Selisih satu piksel (scrollHeight > clientHeight + 1) sudah cukup sebagai pagar.

Ada konsekuensi rendering di server yang perlu dipahami sejak awal. HTML dari server menampilkan teks dalam keadaan ter-clamp tanpa tombol, karena ukuran baru diketahui setelah hydration. Selama satu saat sebelum JavaScript berjalan, teks panjang tampak terpotong tanpa cara membukanya. Kondisi ini dapat diterima; pola serupa juga berlaku di situs video populer.

Komponen VideoDescription

"use client";

import { useEffect, useRef, useState } from "react";

export function VideoDescription({ text }: { text: string }) {
  const [expanded, setExpanded] = useState(false);
  const [truncatable, setTruncatable] = useState(false);
  const ref = useRef<HTMLParagraphElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    // +1 menahan perbedaan pembulatan subpiksel [5]
    setTruncatable(el.scrollHeight > el.clientHeight + 1);
    // ponytail: ukur ulang hanya saat text berubah; jika viewport
    // bisa berubah saat expanded, tambahkan ResizeObserver.
  }, [text]);

  return (
    <div
      data-testid="video-description-box"
      className="rounded-lg bg-gray-100 p-3 text-sm"
    >
      <p
        ref={ref}
        data-testid="video-description"
        className={
          expanded
            ? "line-clamp-none whitespace-pre-line"
            : "line-clamp-3 whitespace-pre-line"
        }
      >
        {text}
      </p>
      {truncatable && (
        <button
          data-testid={
            expanded ? "video-description-less" : "video-description-more"
          }
          onClick={() => setExpanded(!expanded)}
          className="mt-1 font-medium text-blue-600"
        >
          {expanded ? "lebih sedikit" : "...selengkapnya"}
        </button>
      )}
    </div>
  );
}

Tiga detail yang mudah salah. useEffect bergantung hanya pada text, bukan pada expanded; jika expanded ikut sebagai dependency, pengukuran ulang saat teks diperluas akan menghasilkan scrollHeight yang sama dengan clientHeight, sehingga truncatable menjadi false dan tombol menghilang di tengah interaksi. Tombol dirender hanya ketika truncatable bernilai true; deskripsi pendek tidak boleh menampilkan toggle yang tidak melakukan apa-apa. Kelas clamp ditukar, bukan ditempel permanen, sehingga saat diperluas teks mengalir normal tanpa batas tiga baris [6].

Pengujian di jsdom

jsdom tidak menghitung layout, sehingga scrollHeight dan clientHeight selalu bernilai nol dan pengukuran setelah mount tidak pernah menghasilkan overflow. Uji kasus tanpa toggle tetap bisa jalan karena nol tidak pernah lebih besar dari satu. Untuk jalur dengan toggle, offset perlu di-mock; round-trip penuh butuh browser test sungguhan.

Tiga kasus pada commit ini:

  • Teks pendek dalam keadaan collapsed: toggle tidak dirender.
  • Offset di-mock (scrollHeight 120, clientHeight 60): toggle muncul, klik ...selengkapnya memperluas, klik lebih sedikit mengembalikan keadaan awal.
  • Blok atribusi channel di bawah deskripsi tetap dirender utuh setelah perubahan struktur.
import { render, screen } from "@testing-library/react";
import { VideoDescription } from "./VideoDescription";

test("toggle tidak muncul saat teks muat", () => {
  vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(0);
  vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(0);

  render(<VideoDescription text="Deskripsi singkat." />);

  expect(
    screen.queryByTestId("video-description-more")
  ).not.toBeInTheDocument();
});

Pola yang diadopsi dari situs video itu bekerja karena dua keputusan, bukan satu. Clamp menjamin kotak tidak pernah melebihi tiga baris, dan toggle hanya hadir saat pengukuran membuktikan ada teks tersembunyi. Label "Deskripsi" dihapus dari UI; bentuk kotak kelabu yang membulat sudah cukup menjelaskan isi tanpa teks tambahan, dan tingginya sekarang dapat diprediksi untuk deskripsi sepanjang apa pun.

Sources

1. [line-clamp - CSS | MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-clamp) 2. [Element: scrollHeight property | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight) 3. [line-clamp - Tailwind CSS](https://tailwindcss.com/docs/line-clamp)

Sources

  • [4] https://developer.mozilla.org/en-US/docs/Web/CSS/line-clamp — line-clamp, CSS, MDN
  • [5] https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight — Element scrollHeight, MDN
  • [6] https://tailwindcss.com/docs/line-clamp — line-clamp, Tailwind CSS

Artikel terkait