All posts
Bí mật sau layout Pinterest — chỉ là 2 dòng CSS

Bí mật sau layout Pinterest — chỉ là 2 dòng CSS

Thai Le Q.'s avatarThai Le Q.
Table of Contents13 sections

Xây dựng lưới thác nước linh hoạt chỉ với vài dòng CSS — không cần thư viện, không cần JavaScript phức tạp.

1. Masonry Layout là gì?

Masonry layout (hay còn gọi là "lưới thác nước") là kiểu bố cục trong đó các phần tử được sắp xếp theo cột dọc — tương tự cách những viên gạch được xếp lên nhau không đều nhau trong một bức tường. Mỗi item chiếm chiều cao tự nhiên của nó, item tiếp theo lấp đầy khoảng trống phía dưới, tạo ra một bố cục liên tục, mượt mà.

Pinterest là trang web phổ biến nhất sử dụng kiểu layout này — ảnh dọc, ảnh ngang, ảnh vuông đều được xếp hài hòa mà không lãng phí khoảng trắng.

Thuật ngữ "masonry" lấy cảm hứng từ nghề xây gạch (masonry = thợ nề). Những viên gạch được xếp liền kề không đều, tận dụng tối đa không gian — đúng như cách layout này hoạt động.

2. Ba cách xây dựng Masonry Layout

Phương pháp

Hỗ trợ trình duyệt

JS cần thiết?

Độ khó

CSS Columns (khuyến nghị)

Tất cả

Không

Dễ

CSS Grid masonry (thực nghiệm)

Safari only

Không

Trung bình

JavaScript (Masonry.js)

Tất cả

Trung bình

Trong bài này, chúng ta tập trung vào CSS Columns — đơn giản nhất, hoạt động trên mọi trình duyệt hiện đại, và không cần một dòng JavaScript nào để tạo ra bố cục cơ bản.

3. Cấu trúc HTML

Cấu trúc HTML rất đơn giản: một container bọc toàn bộ lưới, bên trong là các card. Mỗi card chứa ảnh và thông tin mô tả.

<!-- Masonry container -->
<div class="masonry">

  <div class="pin">
    <img src="photo.jpg" alt="Mô tả ảnh" loading="lazy" />
    <button class="save-btn">Lưu</button>
  </div>

  <!-- Lặp lại các .pin tiếp theo -->

</div>

4. CSS — Phần quan trọng nhất

Toàn bộ hiệu ứng masonry chỉ đến từ 2 dòng CSS. Phần còn lại là styling thông thường cho card.

/* ĐÂY LÀ PHẦN QUAN TRỌNG NHẤT */
.masonry {
  columns: 5 200px;     /* tối đa 5 cột, tối thiểu 200px mỗi cột */
  column-gap: 12px;   /* khoảng cách giữa các cột */
  padding: 20px 16px;
}

.pin {
  break-inside: avoid;  /* ngăn card bị cắt ngang cột */
  margin-bottom: 12px;
}

Giải thích columns: 5 200px: Trình duyệt sẽ tự tính — tạo tối đa 5 cột, nhưng mỗi cột không được nhỏ hơn 200px. Nếu màn hình hẹp, số cột tự giảm xuống. Hoàn toàn responsive mà không cần media query!

CSS hoàn chỉnh cho card

.pin {
  break-inside: avoid; /* Không cho card bị tách giữa các cột Masonry */
  margin-bottom: 12px; 
  border-radius: 16px; 
  overflow: hidden; /* Ẩn phần nội dung vượt ra ngoài bo góc */
  background: #fff;
  position: relative; /* Làm mốc cho nút Save position: absolute */
  cursor: pointer;
}

.pin-cover {
  display: flex;
  align-items: flex-end; /* Đưa nội dung xuống cuối card */
  padding: 16px;
  transition: filter 0.2s;
}

.pin:hover .pin-cover {
  filter: brightness(0.85); /* Làm tối card khi hover */
}

.pin-content {
  color: #222;
}

.pin-label {
  font-size: 12px;
  opacity: 0.7; /* Giảm độ nổi bật của label */
  margin-bottom: 6px;
}

.pin-title {
  font-size: 20px;
  font-weight: 700;
  line-height: 1.2;
}

.pin-sub {
  margin-top: 8px;
  font-size: 13px;
  opacity: 0.75;
}

/* Nút Save xuất hiện khi hover */
.save-btn {
  position: absolute;
  top: 10px;
  right: 10px;
  background: #e60023;
  color: #fff;
  border: none;
  border-radius: 20px;
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;

  opacity: 0; /* Ẩn mặc định */
  transition: opacity 0.2s;
}

.pin:hover .save-btn {
  opacity: 1; /* Hiện nút Save khi hover card */
}

5. JavaScript — Render card động

Nếu bạn muốn render card từ dữ liệu (ví dụ: từ API), dưới đây là đoạn JavaScript mẫu:

Vì là mục đích testing nên đang sử dụng thẻ div và sử dụng chiều cao cố định để hiện thị, cần sửa lại bằng thẻ <img/> khi sử dụng thực tế.

const items = [
      { h: 260, color: "#c8e6c9", label: "Nature photography", sub: "Save to board", icon: "ti-photo", title: "Misty forest morning" },
      { h: 180, color: "#bbdefb", label: "Architecture", sub: "Save to board", icon: "ti-building", title: "Glass facade reflections" },
      //Thêm hình ảnh tại đây
    ];

    const grid = document.getElementById("grid");

    items.forEach((item) => {
      const pin = document.createElement("div");
      pin.className = "pin";

      pin.innerHTML = `
    <div
      class="pin-cover"
      style="
        height:${item.h}px;
        background:${item.color};
      "
    >
      <div class="pin-content">
        <div class="pin-label">${item.label}</div>
        <div class="pin-title">${item.title}</div>
        <div class="pin-sub">${item.sub}</div>
      </div>

      <button class="save-btn">Lưu</button>
    </div>
  `;

      grid.appendChild(pin);
    });

Luôn thêm thuộc tính loading="lazy" cho thẻ <img> để trình duyệt chỉ tải ảnh khi chúng xuất hiện trong vùng nhìn thấy. Điều này cải thiện đáng kể hiệu suất trang khi có nhiều ảnh.

6. Responsive không cần Media Query

Một trong những ưu điểm lớn nhất của columns là tự động responsive. Với columns: 5 200px, trình duyệt sẽ:

  1. Màn hình lớn (≥1000px) Hiển thị 5 cột đầy đủ — mỗi cột khoảng 200px. Trải nghiệm Pinterest chuẩn.

  2. Tablet (600px – 1000px) Tự giảm xuống 3–4 cột. Layout vẫn đẹp, không bị vỡ.

  3. Mobile (dưới 600px) Thu gọn còn 2 cột. Người dùng vẫn thấy được nội dung thoải mái.

CSS — Responsive tùy chỉnh (nếu cần)

/* Mặc định: tự động responsive */
.masonry { columns: 5 200px; }

/* Nếu muốn kiểm soát chi tiết hơn */
@media (max-width: 768px) {
  .masonry { columns: 2; }
}

@media (max-width: 480px) {
  .masonry { columns: 1; } /* feed dạng đơn trên điện thoại nhỏ */
}

8. Mẹo và những lưu ý quan trọng

Luôn đặt height: auto cho ảnh

Nếu bạn cứng nhắc chiều cao ảnh, masonry sẽ mất tác dụng vì tất cả card đều bằng nhau. Để ảnh giữ tỷ lệ tự nhiên chính là bí quyết tạo ra cái "thác nước" đặc trưng.

Đừng dùng display: flex hoặc display: grid cho container

columns là thuộc tính riêng, không tương thích với flex/grid. Để container ở display: block (mặc định) là đủ.

Sử dụng column-fill: balance (tùy chọn)

CSS

.masonry {
  columns: 5 200px;
  column-gap: 12px;
  column-fill: balance;  /* phân phối đều nội dung giữa các cột */
}

Animate card khi tải

CSS — Hiệu ứng xuất hiện

.pin {
  animation: fadeUp 0.4s ease both;
}

.pin:nth-child(odd)  { animation-delay: 0.05s; }
.pin:nth-child(even) { animation-delay: 0.1s; }

@keyframes fadeUp {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* giảm chuyển động */
@media (prefers-reduced-motion: reduce) {
  .pin { animation: none; }
}

9. Tổng kết

Masonry layout với CSS Columns là một trong những kỹ thuật đơn giản nhưng mang lại hiệu ứng thị giác ấn tượng nhất. Chỉ với 2 dòng CSS bạn đã có một layout hoàn toàn responsive, không cần JavaScript, không cần thư viện bên ngoài.

Tóm tắt nhanh: Dùng columns: N Wpx trên container và break-inside: avoid trên từng card. Để ảnh dùng height: auto. Thêm hover effect qua opacityfilter. Vậy là xong!