Bố cục CSS và thiết kế đáp ứng

  1. 1 Kiến thức cơ bản
    1. Nên sử dụng chế độ bố cục nào?
    2. Nguyên tắc làm việc
  2. 2 Flexbox
  3. 3 Lưới và lưới phụ
    1. Ví dụ về mã: lưới và lưới phụ
  4. 4 Truy vấn vùng chứa
    1. Ví dụ về mã: kiểu chữ linh hoạt sử dụng đơn vị truy vấn vùng chứa
  5. 5 Lớp phủ gốc, vị trí neo và bối cảnh xếp chồng
  6. 6 Theo dõi phần tử tràn và độ ổn định của bố cục
  7. 7 Cơ chế khung nhìn và phân phối bản nhạc
  8. 8 làn lưới (còn gọi là bố cục dạng khối)

1 Kiến thức cơ bản

Hãy dựa vào công cụ bố cục của trình duyệt khi có thể để có hiệu suất tốt hơn. Hãy sử dụng kích thước nội tại, thuộc tính logic và aspect-ratio trước khi dùng đến kích thước được cố định giá trị trong mã hoặc truy vấn đa phương tiện phức tạp.

1.1 Nên sử dụng chế độ bố cục nào?

Xem sơ đồ quyết định từ trên xuống dưới và dừng lại ở kết quả phù hợp đầu tiên. Xin lưu ý rằng các bố cục có thể được lồng vào nhau và mỗi quyết định đều dựa trên trường hợp sử dụng cho vùng chứa đó.

  1. Đó có phải là một hàng HOẶC cột mục đơn giản không? Sử dụng flexbox – 1D, ưu tiên nội dung, nội dung phân phối dọc theo một trục duy nhất.
  2. Phần tử lồng nhau có cần phải xếp thẳng hàng với các dải của lưới cấp ông bà không? Sử dụng subgrid – 2D, ưu tiên mối quan hệ, kế thừa các dải của phần tử mẹ để các phần tử cháu có thể căn chỉnh trên các phần tử đồng cấp.
  3. Đó có phải là một trang hoặc cấu trúc thành phần phức tạp có cả hàng VÀ cột không? Sử dụng grid (lưới) – 2D, ưu tiên bố cục, bạn xác định khung và nội dung sẽ lấp đầy khung đó.
  4. Nội dung có phải là một dòng văn xuôi dài nên được chia thành các cột cân bằng không? Sử dụng nhiều cột – luồng 1D, kiểu báo.
  5. Các mặt hàng có chiều cao khác nhau có cần được đóng gói chặt không? Sử dụng grid với grid-auto-flow: dense ngay hôm nay; chỉ sử dụng bố cục khối xây gốc (hay còn gọi là "đường lưới") khi bố cục này được cung cấp trong mục tiêu Baseline (xem §8).
  6. Một phần tử có cần nổi phía trên trang và luôn được liên kết về mặt không gian với một trình kích hoạt, ngay cả khi vượt qua ranh giới DOM hoặc bối cảnh xếp chồng không? Sử dụng vị trí neo — anchor-name trên trình kích hoạt, position-anchor trên lớp phủ (xem §5).

1.2 Nguyên tắc hoạt động

  • Sử dụng các thuộc tính logic (inline-size, block-size, margin-inline, padding-block, inset-inline-start) cho kích thước và khoảng cách bố cục — xem CSS: Kiến trúc hiện đại để biết thông tin đầy đủ.
  • Áp dụng mô hình tư duy ưu tiên nội dung so với ưu tiên bố cục: flexbox khi các mục quyết định luồng, lưới khi bạn xác định khung trước.
  • Sử dụng các thuộc tính viết tắt place-* (place-content, place-items, place-self) để căn chỉnh trên cả hai trục trong một khai báo.
  • Sử dụng kích thước nội tại (min-content, max-content, fit-content()) và các dải linh hoạt (fr, minmax()) trước khi sử dụng width/height cố định — ít truy vấn đa phương tiện hơn, bố cục linh hoạt hơn.
  • KHÔNG sử dụng overflow: hidden để che giấu các vấn đề về bố cục — hãy khắc phục các vấn đề đó tại nguồn. Ví dụ: một số phần tử được thay thế như <canvas>, <img>, <svg>, <video>, <iframe> sử dụng display: inline theo mặc định, điều này có thể thêm khoảng trống trong một số bố cục nhất định. Thay vì cắt khoảng trống bằng overflow, hãy xoá khoảng trống đó bằng cách áp dụng giá trị hiển thị bên ngoài khối (ví dụ: display: block, grid, flex, v.v.).
  • Sử dụng aspect-ratio để dành không gian cho nội dung nghe nhìn và ngăn bố cục thay đổi trước khi thành phần tải.
.sidebar       { inline-size: max-content; }    /* Size to longest unbreakable token. */
.main-content  { inline-size: fit-content; }    /* Grow to available space, no further. */
.media         { aspect-ratio: 16 / 9; inline-size: 100%; block-size: auto; }
body.centered  { display: grid; place-content: center; min-block-size: 100dvb; }

Để biết thông tin về calc-size() và kích thước nội tại nhận biết quy tắc ràng buộc, hãy xem bài viết Tính toán bằng kích thước nội tại.

2 Flexbox

Bố cục một chiều – các mục di chuyển dọc theo một trục chính và căn chỉnh trên trục chéo. Hãy sử dụng thành phần này cho thanh điều hướng, thanh công cụ, hàng mục và mọi cách phân phối một hàng hoặc một cột.

Nên làm:

  • Thiết lập ngữ cảnh bằng display: flex và đặt trục chính bằng flex-direction (mặc định là row).
  • Sử dụng flex-wrap: wrap bất cứ khi nào có thể xảy ra tình trạng tràn — nowrap không có overflow: auto/hidden sẽ tràn trên khung nhìn hẹp.
  • Xếp lớp flex-wrap: balance sau flex-wrap: wrap trên các hàng được gói của các mục nhỏ gọn (chẳng hạn như đám mây thẻ, khối bộ lọc, nhóm nút hoặc đường liên kết điều hướng) để phân phối các mục đồng đều trên các dòng và ngăn một mục đơn lẻ bị tách riêng trên hàng cuối cùng; các trình duyệt không hỗ trợ sẽ quay lại wrap.
  • Sử dụng viết tắt flex <grow> <shrink> <basis> (ví dụ: flex: 1 1 250px) trên các mục thay vì đặt flex-grow/flex-shrink/flex-basis riêng lẻ.
  • Sử dụng gap (hoặc row-gap/column-gap dạng dài) để tạo khoảng cách giữa các mục thay vì lề con.
  • Thêm tiền tố căn chỉnh vị trí bằng safe (ví dụ: align-items: safe center) để nội dung có thể làm tâm điểm không bị cắt bớt khi vùng chứa hẹp hơn nội dung.
  • Đẩy một mục duy nhất đến cuối trục chính bằng margin-inline-start: auto (hoặc margin-block-start: auto) — đó là lối thoát tiêu chuẩn.
  • Ghi đè căn chỉnh trục chéo cho mỗi mục bằng align-self.
  • Sử dụng align-items để căn giữa tất cả các mục trên trục chéo; sử dụng margin: auto trên một mục để căn giữa mục đó trên cả hai trục một cách độc lập; chỉ sử dụng align-content khi vùng chứa xuống dòng và có thêm không gian giữa các hàng.
  • Đặt min-inline-size: 0 (hoặc min-width: 0) trên các mục linh hoạt chứa nội dung dài không thể ngắt (URL, mã, chuỗi dài) — các mục linh hoạt sẽ không thu nhỏ dưới kích thước nội dung theo mặc định, gây ra tình trạng tràn.

Không nên:

  • Đừng dùng justify-self trên các mục linh hoạt — thuộc tính này chỉ hoạt động trên bố cục lưới, khối và bố cục được định vị tuyệt đối. Thay vào đó, hãy sử dụng lề tự động.
  • Đừng sử dụng order hoặc flex-direction: *-reverse để sắp xếp lại nội dung tương tác. Các thuộc tính này chỉ thay đổi thứ tự hiển thị; thứ tự DOM vẫn điều khiển tiêu điểm tuần tự, vì vậy, luồng thẻ bàn phím sẽ không khớp với những gì người dùng nhìn thấy.
  • Đừng nhầm lẫn space-around (khoảng cách bằng một nửa ở hai đầu) với space-evenly (khoảng cách bằng nhau ở trước, giữa và sau).
  • Đừng quên việc lật trục: khi flex-direction: column, justify-content căn chỉnh trên trục khối và align-items căn chỉnh trên trục cùng dòng — ngược lại với mặc định.
  • Đừng đặt kích thước cho cả vùng chứa và các phần tử con của vùng chứa để lấp đầy lẫn nhau — đây là nguyên nhân phổ biến gây ra tình trạng tràn và kết quả không mong muốn. Cung cấp cho một bên một kích thước xác định.
  • Đừng đặt cả flex-basis và width/inline-size trên cùng một mục — flex-basis được ưu tiên trong ngữ cảnh linh hoạt và width bị bỏ qua. Sử dụng flex-basis (hoặc viết tắt là flex) làm nguồn đáng tin cậy duy nhất để định cỡ các mục linh hoạt.
.card-grid        { display: flex; flex-flow: row wrap; gap: 1rem; }
.card-item        { flex: 1 1 250px; }                  /* grow, shrink, basis */
.card-item-action { margin-inline-start: auto; }        /* Push to main-axis end. */
.toolbar          { display: flex; align-items: safe center; }
.tag-list         { display: flex; flex-wrap: wrap; flex-wrap: balance; gap: 0.5rem; }

3 Lưới và lưới phụ

Bố cục hai chiều — xác định rõ ràng các hàng VÀ cột hoặc để công cụ tự suy ra. Lưới phụ cho phép lưới lồng nhau kế thừa các dải của lưới mẹ để các lưới con căn chỉnh trên các lưới đồng cấp.

Chọn tính năng lưới:

  • Bạn có biết chính xác mình cần bao nhiêu cột không?
    • Có — sử dụng các bản nhạc có nội dung phản cảm (grid-template-columns: 200px 1fr, repeat(3, 1fr), v.v.)
    • Các cột khác nhau có cần kích thước khác nhau không (thanh bên + chính, tiêu đề trải dài trên tất cả)? → sử dụng grid-template-areas cho các vùng có tên và dễ đọc
    • Tất cả các cột có đồng nhất hay được định vị hoàn toàn theo số dòng không? → sử dụng repeat(N, ...) hoặc các đường kẻ được đặt tên
    • Không (thích ứng, không biết số lượng mục) — sử dụng repeat(auto-fit, minmax(min, 1fr))
    • Các mục ở hàng cuối cùng có nên kéo giãn để lấp đầy không gian còn lại không? → auto-fit
    • Các dải trống ở hàng cuối có nên giữ kích thước tối thiểu (giữ lại các ô trống trong cột) không? → auto-fill
  • Bạn có cần đặt một mặt hàng tại một vị trí cụ thể không?
    • Có – sử dụng grid-column: <start> / <end> hoặc grid-area: <name>
    • Không (chỉ trải dài trên nhiều kênh, vị trí luồng không quan trọng) — sử dụng grid-column: span <n>
  • Các phần tử con có cần kế thừa kích thước bản nhạc của lưới mẹ (căn chỉnh cạnh không đều giữa các phần tử đồng cấp) không?
    • Có — sử dụng lưới phụ trên trục bị ảnh hưởng
    • Số lượng phần tử con trên mỗi ô có thay đổi không? → lưới phụ chỉ một trục; sử dụng grid-auto-rows/grid-auto-columns cho trục còn lại
    • Số lượng trẻ em có cố định không? → lưới phụ trên cả hai trục đều được
    • Không — lưới chuẩn, không cần lưới phụ

Nên làm:

  • Thiết lập bối cảnh bằng display: grid.
  • Sử dụng grid-template-areas cho bố cục phức tạp ở cấp trang – tên vùng là tự ghi lại và nội dung khai báo có thể được căn chỉnh theo hàng và cột để dễ đọc.
  • Sử dụng repeat(auto-fit, minmax(200px, 1fr)) cho lưới thẻ thích ứng giúp kéo giãn các dải ô đã lấp đầy để lấp đầy hàng hoặc auto-fill để giữ nguyên các dải ô lặp lại trống ở kích thước tối thiểu.
  • Sử dụng fr để phân phối dải theo tỷ lệ và minmax(min, max) cho các dải linh hoạt nhưng có giới hạn.
  • Đặt vị trí các mục bằng grid-column: span <n> để định kích thước trên các dải, grid-column: <start> / <end> để đặt tại các dòng cụ thể hoặc grid-area: <name> cho các vùng được đặt tên.
  • Sử dụng lưới phụ (grid-template-columns: subgrid hoặc grid-template-rows: subgrid) để giải quyết vấn đề "cạnh không bằng phẳng" trong danh sách thẻ – các phần tử nội bộ như tiêu đề, siêu dữ liệu và lời kêu gọi hành động (CTA) xếp hàng ngang với các phần tử đồng cấp.
  • Ghép nối nội dung khai báo lưới phụ với nội dung khai báo grid-template-rows/-columns rõ ràng trước đó làm phương án dự phòng cùng tầng cho các trình duyệt cũ.

Không nên:

  • Đừng mong đợi kích thước bản nhạc auto-fit/auto-fill đến từ nội dung mục — kích thước này đến từ đối số kích thước repeat().
  • Đừng sử dụng grid-auto-flow: dense trên nội dung tương tác. Thuộc tính này sắp xếp các mục một cách hiệu quả nhưng lại sắp xếp lại thứ tự hiển thị, làm gián đoạn luồng thẻ bàn phím theo thứ tự DOM.
  • Đừng áp dụng lưới phụ cho cả hai trục khi số lượng phần tử con có thể thay đổi. Các phần tử bổ sung sẽ nằm trong kênh cuối cùng; hãy sử dụng grid-auto-rows/grid-auto-columns cho trục ngầm định.
  • Đừng nhầm lẫn justify-items/align-items (căn chỉnh nội dung của mục trong dải của mục đó) với justify-content/align-content (căn chỉnh các dải lưới trong vùng chứa). Việc sử dụng sai sẽ không có hiệu lực.
  • Đừng sử dụng repeat(auto-fit/auto-fill, ...) mà không có inline-size xác định trên vùng chứa — bên trong display: inline-grid hoặc một mục linh hoạt không có kích thước, vùng chứa không có chiều rộng để chia, khiến số lượng bản nhạc không thể dự đoán được.

3.1 Ví dụ về mã: lưới và lưới phụ

Page shell: <main class="page-layout"> contains <header>, <aside>, a <section class="card-grid"> with <div class="card"> children, and <footer>.

/* Align grid-template-areas in rows and columns for readability. */
.page-layout {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-areas:
    "header  header  header"
    "sidebar main    main"
    "footer  footer  footer";
  gap: 1.5rem;
}

header  { grid-area: header; }
aside   { grid-area: sidebar; }
footer  { grid-area: footer; }

.card-grid {
  grid-area: main;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  grid-template-rows: auto 1fr; /* title block, body block */
  gap: 1rem;
}

.card {
  grid-row: span 2;
  display: grid;
  /* Same-cascade fallback: ignored when subgrid is supported. */
  grid-template-rows: auto 1fr;
  grid-template-rows: subgrid;
}

4 Truy vấn vùng chứa

Truy vấn kích thước (hoặc kiểu được tính toán) của vùng chứa gốc thay vì khung nhìn. Mô hình tư duy: truy vấn vùng chứa = ngữ cảnh thành phần; truy vấn đa phương tiện = bố cục trang chung và lựa chọn ưu tiên của người dùng (prefers-color-scheme, prefers-reduced-motion).

Nên làm:

  • Thiết lập bối cảnh vùng chứa bằng container-type: inline-size (chỉ truy vấn chiều rộng) hoặc container-type: size (cả hai trục) trên một trình bao bọc trước khi có thể truy vấn các thành phần con của trình bao bọc đó.
  • Đặt tên cho vùng chứa bằng container-name (hoặc viết tắt container: container: card / inline-size) khi các bối cảnh lồng nhau có thể xung đột.
  • Thêm các đơn vị truy vấn vùng chứa vào việc tính toán kiểu chữ và khoảng cách linh hoạt: cqi/cqb (nội tuyến/khối logic), cqw/cqh (vật lý), cqmin/cqmax.
  • Cung cấp cho vùng chứa một block-size xác định bất cứ khi nào container-type: size được sử dụng — nếu không có, các phần tử con sẽ thu gọn vì vùng chứa kích thước buộc vùng chứa bỏ qua nội dung của nó.

Không nên:

  • Đừng sử dụng block-size làm giá trị container-type vì giá trị này không hợp lệ. Sử dụng size cho cả hai trục.
  • Đừng mong đợi kích thước nội tại của thành phần con sẽ ảnh hưởng đến vùng chứa sau khi khai báo container-type. Vùng chứa được tính toán như thể không có phần tử con sau khi tính năng chứa được kích hoạt.
  • Đừng dựa vào các đơn vị truy vấn vùng chứa bên trong các phần tử con của một phần tử cấp trên không đủ điều kiện; các đơn vị này sẽ quay về khung nhìn nhỏ (svw/svh).

4.1 Ví dụ về mã: kiểu chữ linh hoạt sử dụng đơn vị truy vấn vùng chứa

.card-wrapper {
  container: card / inline-size; /* shorthand for container-name / container-type */
}

@container card (inline-size > 400px) {
  .content {
    display: flex;
    gap: 2rem;
  }
}

.title {
  /* Fluid type bound to the container width, not the viewport. */
  font-size: clamp(1rem, 4cqi, 2rem);
}

Để biết các mẫu định kiểu thích ứng dựa trên thành phần, hãy xem phần Định kiểu nhận biết kích thước và Điều chỉnh tỷ lệ linh hoạt.

5 Lớp phủ gốc, vị trí neo và bối cảnh xếp chồng

Trường hợp sử dụng từng thành phần gốc của lớp phủ:

  • Sử dụng popover cho giao diện người dùng tạm thời, không có phương thức (cửa sổ bật lên, thông báo ngắn, chú thích) – nằm ở lớp trên cùng, không cần quản lý z-index.
  • Sử dụng <dialog> với .showModal() cho các hoạt động tương tác theo phương thức yêu cầu bẫy tiêu điểm và phông nền không hoạt động.
  • Đừng kết hợp popover và .showModal() trên cùng một phần tử – đây là các trạng thái thời gian chạy loại trừ lẫn nhau.

Định vị neo (bố cục không gian của lớp phủ):

  • Sử dụng position-area (hoặc anchor() trên phần lồng ghép) và anchor-size() để định vị và định kích thước lớp phủ tương ứng với điều kiện kích hoạt.
  • Sử dụng position-try-fallbacks: flip-block (hoặc flip-inline) để cho phép trình duyệt định vị lại khi lớp phủ tràn khung nhìn.
  • Đừng kết hợp từ khoá vật lý và từ khoá logic trong một giá trị position-area duy nhất — hãy chọn một hệ toạ độ.
  • Phát hiện tính năng bằng @supports (anchor-name: --x) và cung cấp tính năng dự phòng vị trí tuyệt đối.

Để biết thông tin chi tiết đầy đủ về cách triển khai, chiến lược polyfill và tham chiếu giá trị popover, hãy xem bài viết Hộp thoại khai báo và điều khiển cửa sổ bật lên và Chú giải công cụ nhận biết vị trí. Để biết cách áp dụng tính năng định vị neo cho trình đơn thả xuống, trình đơn thả ra và chỉ báo thẻ, hãy xem phần Trình đơn thả xuống nhận biết khung nhìn và Đường gạch dưới của thẻ định vị neo.

6 Theo dõi phần tử tràn và độ ổn định của bố cục

Quản lý các thay đổi về bố cục, thanh cuộn và việc cắt bớt một cách dễ đoán.

Nên làm:

  • Sử dụng overflow: auto để thanh cuộn chỉ xuất hiện khi nội dung thực sự bị tràn.
  • Sử dụng overflow: clip để cắt nội dung mà không cần thiết lập vùng chứa cuộn; chọn tràn bằng overflow-clip-margin.
  • Sử dụng scrollbar-gutter: stable để dành không gian cho thanh cuộn và ngăn bố cục thay đổi khi nội dung tăng lên.
  • Sử dụng overscroll-behavior: contain (hoặc none) trên các vùng cuộn lồng nhau (chẳng hạn như thanh bên, cửa sổ phụ và danh sách) để ngăn chuỗi cuộn nổi lên trang hoặc overscroll-behavior: chain trên trình bao bọc cuộn trung gian (chẳng hạn như vuốt để hiển thị các bản nhạc) khi thao tác cuộn thừa sẽ truyền đến trình cuộn gốc mà không kích hoạt hiệu ứng nảy cục bộ.
  • Sử dụng bộ ba -webkit-line-clamp + display: -webkit-box + -webkit-box-orient: vertical để cắt bớt nhiều dòng — bất kể tiền tố, mẫu này được chỉ định đầy đủ và không bị ngừng sử dụng. Khai báo viết tắt line-clamp không có tiền tố cùng với thuộc tính đó; các trình duyệt chưa hỗ trợ sẽ bỏ qua thuộc tính này mà không gây hại. Không nên:

  • Đừng sử dụng overflow: scroll khi auto có thể làm được — scroll buộc thanh cuộn ngay cả khi không có gì để cuộn.

  • Đừng dùng overflow: hidden khi bạn chỉ muốn cắt bớt — hidden thiết lập một vùng chứa cuộn có thể cuộn theo phương thức lập trình.

.scrollable-list {
  max-block-size: 400px;
  overflow-y: auto;
  scrollbar-gutter: stable;       /* Reserve scrollbar space. */
  overscroll-behavior: contain;   /* No scroll chaining into the page. */
}

.snippet {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  line-clamp: 3;                  /* Ignored where unsupported. */
  overflow: clip;
}

Để tìm hiểu chi tiết về overflow: clip và overflow-clip-margin, hãy xem phần Kiểm soát cắt bớt phần tràn. Để biết cách chuyển giao thao tác cuộn lồng nhau bằng overscroll-behavior: chain trong giao diện người dùng vuốt để hiện, hãy xem phần Vuốt để hiện. Để biết màu sắc, kích thước và giao diện của thanh cuộn, hãy xem phần Tuỳ chỉnh màu sắc hoặc độ dày của thanh cuộn, Chế độ tối và Điều chỉnh thanh cuộn theo lựa chọn ưu tiên về độ tương phản cao.

7 Cơ chế khung nhìn và phân phối bản nhạc

  • Sử dụng dvh/dvw cho vùng chứa bố cục dành cho thiết bị di động phải tính đến sự thay đổi giao diện người dùng của trình duyệt (thu gọn/mở rộng thanh URL).
  • Đừng sử dụng 100vw cho bố cục có chiều rộng tối đa vì thuộc tính này bỏ qua chiều rộng của thanh cuộn và gây ra tình trạng tràn theo chiều ngang. Thay vào đó, hãy sử dụng 100%, 100dvw hoặc 100svw.

Để biết điểm truy cập thiết kế đáp ứng (đơn vị khung nhìn động, dành không gian cho nội dung nghe nhìn, kiểu chữ linh hoạt), hãy xem phần Thiết kế đáp ứng.

8 đường lưới (còn gọi là kiểu xếp gạch)

Thông số kỹ thuật này đang được phát triển dưới tên "làn đường lưới" (ví dụ: display: grid-lanes), trong khi các nguyên mẫu thử nghiệm trước đó sử dụng grid-template-rows: masonry.

Nên làm:

  • Sử dụng lưới với grid-auto-flow: dense để đóng gói chặt chẽ ngay hôm nay, chấp nhận rằng thứ tự DOM có thể không khớp với thứ tự hình ảnh.
  • Sử dụng nhiều cột (columns: 3; column-gap: 1rem) cho luồng giống như khối xây có nhiều nội dung khi các mục là các đoạn tài liệu thay vì các thẻ có trọng số bằng nhau.
  • Chỉ coi grid-template-rows: masonry là một cải tiến tăng dần – phát hiện tính năng bằng @supports.

Không nên:

  • Đừng cung cấp grid-template-rows: masonry dưới dạng yêu cầu bắt buộc cho đến khi mục tiêu Cơ sở của bạn bắt kịp.
.gallery       { columns: 3 200px; column-gap: 1rem; }
.gallery > *   { break-inside: avoid; margin-block-end: 1rem; }

@supports (grid-template-rows: masonry) {
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    grid-template-rows: masonry;
    gap: 1rem;
    columns: unset;
  }
}