Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 18 — Fluid Typography với clamp()

Web Design Basics Bài 18/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn vừa hoàn thành thiết kế một landing page cho khách hàng. Trên màn hình laptop 15 inch của bạn, tiêu đề hero hiện ra đẹp lung linh ở kích thước 48px. Khách hàng mở trên điện thoại — chữ to chật cứng, tràn ra ngoài màn hình, phải cuộn ngang mới đọc hết. Bạn vội thêm một @media breakpoint cho mobile, giảm xuống 24px. Ổn rồi! Nhưng rồi một người khác mở trên iPad ở khoảng giữa hai breakpoint, và tiêu đề trông... lạc lõng — quá nhỏ so với khoảng không gian rộng của tablet.

Đây là một trong những vấn đề kinh điển mà mọi web designer đều gặp: chữ "nhảy" gãy khúc khi chuyển giữa các kích thước màn hình. Bạn đặt 24px ở mobile, 48px ở desktop, nhưng ở khúc giữa thì chữ bị khựng lại ở một con số, rồi đột ngột nhảy sang con số khác ngay tại điểm breakpoint. Trải nghiệm không mượt, và đặc biệt với thị trường Việt Nam — nơi người dùng truy cập web từ vô vàn loại điện thoại Android màn hình to nhỏ khác nhau, từ Galaxy A nhỏ gọn đến những chiếc tablet giá rẻ — sự "nhảy chữ" này càng lộ rõ.

Bài học hôm nay sẽ cho bạn một công cụ giải quyết triệt để vấn đề này: fluid typography (typography chất lỏng) với hàm CSS clamp(). Đây không phải là kiến thức trang trí — nó là kỹ năng nền tảng giúp typography của bạn co giãn mượt mà theo mọi kích thước màn hình mà chỉ cần viết một dòng CSS duy nhất, không cần hàng tá breakpoint. Sau bài này, bạn sẽ nhìn typography theo một tư duy hoàn toàn khác: không còn là những con số cố định, mà là những giá trị biết "thở" theo viewport.

Khái niệm cốt lõi

Vấn đề với breakpoints cố định

Cách làm truyền thống là dùng media query để gán kích thước chữ theo từng dải màn hình:

h1 {
  font-size: 24px; / mobile /
}

@media (min-width: 768px) { h1 { font-size: 36px; / tablet / } }

@media (min-width: 1200px) { h1 { font-size: 48px; / desktop / } }

Cách này hoạt động, nhưng có hai nhược điểm lớn. Thứ nhất, chữ nhảy bậc thang: ở màn hình rộng 767px chữ là 24px, nhưng chỉ cần rộng thêm 1px thành 768px, chữ đột ngột nhảy lên 36px. Sự thay đổi không liên tục này khiến thiết kế trông thiếu mượt mà, đặc biệt khi người dùng xoay ngang điện thoại hoặc kéo giãn cửa sổ trình duyệt. Thứ hai, bạn phải bảo trì nhiều điểm cố định — mỗi heading, mỗi đoạn text quan trọng đều cần vài ba breakpoint riêng, khiến file CSS phình to và khó kiểm soát.

Giải pháp: hàm clamp()

clamp() là một hàm CSS nhận ba tham số và trả về một giá trị "bị kẹp" trong một khoảng:

clamp(MIN, PREFERRED, MAX)
  • MIN — giá trị nhỏ nhất, chữ không bao giờ nhỏ hơn mức này (bảo vệ mobile khỏi chữ quá bé).
  • PREFERRED — giá trị "ưa thích", thường là một biểu thức co giãn theo viewport.
  • MAX — giá trị lớn nhất, chữ không bao giờ vượt mức này (bảo vệ desktop khỏi chữ quá to).
Trình duyệt sẽ tính giá trị PREFERRED. Nếu nó nhỏ hơn MIN thì dùng MIN; nếu lớn hơn MAX thì dùng MAX; còn nằm giữa thì dùng chính nó. Ví dụ cốt lõi:

h1 {
  font-size: clamp(1.5rem, 1rem + 2.5vw, 3rem);
}

Dịch ra: chữ này nhỏ nhất là 1.5rem (24px), to nhất là 3rem (48px), và ở khoảng giữa nó co giãn liên tục, mượt mà theo chiều rộng viewport. Không còn nhảy bậc. Một dòng thay cho cả ba khối media query ở trên.

Hiểu phần PREFERRED — bí mật nằm ở đơn vị vw

Phần giữa 1rem + 2.5vw là linh hồn của fluid typography. Đơn vị vw (viewport width) bằng 1% chiều rộng cửa sổ trình duyệt. Trên màn hình rộng 1000px thì 1vw = 10px, 2.5vw = 25px. Khi viewport thay đổi, giá trị này thay đổi theo — đó là cái tạo nên sự "chất lỏng".

Vậy tại sao không viết thẳng font-size: 2.5vw mà phải cộng thêm 1rem? Vì nếu chỉ dùng vw thuần, chữ sẽ co giãn quá mạnh và người dùng không thể zoom chữ bằng cài đặt trình duyệt (một vấn đề accessibility nghiêm trọng). Khi kết hợp một phần cố định bằng rem với một phần vw, bạn vừa có sự co giãn theo màn hình, vừa giữ được khả năng zoom — rem tôn trọng cài đặt cỡ chữ của người dùng. Đây là lý do công thức chuẩn luôn có dạng phần-rem + phần-vw.

Công thức tính phần PREFERRED chính xác

Bạn không cần đoán mò con số. Có công thức toán học để tính phần preferred sao cho chữ chạm đúng MIN tại một viewport nhỏ và chạm đúng MAX tại một viewport lớn. Giả sử:

  • Tại viewport 375px (điện thoại nhỏ), bạn muốn chữ = 24px.
  • Tại viewport 1280px (desktop), bạn muốn chữ = 48px.
Độ dốc (slope) = (48 − 24) / (1280 − 375) = 24 / 905 ≈ 0.0265, tức 2.65vw. Phần cố định (intercept) tính bằng: 24 − 0.0265 × 375 = 24 − 9.94 ≈ 14.06px ≈ 0.88rem. Kết quả:

font-size: clamp(1.5rem, 0.88rem + 2.65vw, 3rem);

Nghe có vẻ rối, nhưng tin tốt là bạn không phải tính tay. Công cụ như Utopia.fyi hay Fluid Type Scale Calculator sẽ tạo ra dòng clamp() hoàn chỉnh chỉ cần nhập min/max. Tôi sẽ hướng dẫn cách dùng ở phần sau.

Tình huống thực tế

Ví dụ 1 — Tiki và bài toán hero heading trên muôn vàn điện thoại Android

Hãy hình dung đội thiết kế của một sàn thương mại điện tử lớn như Tiki đang làm trang khuyến mãi cho đợt sale 11/11. Tiêu đề hero "Sale Sập Sàn — Giảm Đến 50%" cần nổi bật. Trên Figma, designer thiết kế ở 48px cho desktop và 28px cho mobile.

Vấn đề: lượng truy cập di động của Tiki chủ yếu đến từ điện thoại Android với chiều rộng màn hình trải dài từ 360px (máy phổ thông) đến 430px (máy màn hình lớn) và cả tablet 768px. Nếu dùng breakpoint cứng, ở dải 600–768px (điện thoại lớn nằm ngang, tablet nhỏ) chữ vẫn bị kẹt ở cỡ mobile 28px, trông quá nhỏ và lọt thỏm. Khi đội dev áp dụng clamp(1.75rem, 1.2rem + 3vw, 3rem), tiêu đề tự động co giãn liền mạch: ở 360px nó là khoảng 28px, lên 768px nó đã là khoảng 41px, và đạt trần 48px trên desktop. Bài học rút ra: với thị trường có sự phân mảnh thiết bị cao như Việt Nam, fluid typography không phải là "đồ chơi xịn" mà là giải pháp thực dụng giúp typography luôn cân đối trên mọi máy mà không cần đội dev viết hàng chục breakpoint.

Ví dụ 2 — Startup SaaS và sự cố chữ "khổng lồ" trên màn hình 4K

Một startup SaaS giả định tên VietFlow làm phần mềm quản lý công việc. Một bạn dev mới, hào hứng với fluid typography, đã đặt tiêu đề bằng font-size: 5vw thuần (không dùng clamp). Trên laptop mọi thứ ổn. Nhưng khi giám đốc demo sản phẩm cho nhà đầu tư trên một màn hình 4K rộng 3840px, tiêu đề phình to thành 192px (5% của 3840), chiếm gần hết màn hình, trông như poster phim hơn là phần mềm chuyên nghiệp. Buổi demo trở nên ngượng ngùng.

Sửa lại bằng clamp(2rem, 1rem + 3vw, 4rem), tiêu đề bị "kẹp" trần ở 4rem (64px) dù màn hình có rộng đến đâu. Bài học rút ra: phần MAX trong clamp() chính là tấm lưới an toàn. Đừng bao giờ dùng vw thuần cho font-size; luôn bọc trong clamp() để có cả sàn lẫn trần. Một con số quá lớn trên màn hình rộng cũng tệ hại không kém một con số quá nhỏ trên mobile.

Ví dụ 3 — Blog tin tức và độ đọc của body text

Một tòa soạn báo điện tử muốn body text bài viết dễ đọc trên mọi thiết bị. Họ ban đầu để cố định 16px khắp nơi. Trên mobile thì ổn, nhưng độc giả lớn tuổi đọc trên màn hình desktop lớn phàn nàn chữ hơi nhỏ và mỏi mắt. Đội thiết kế áp dụng fluid cho body: clamp(1rem, 0.95rem + 0.3vw, 1.2rem). Để ý phần vw ở đây rất nhỏ (0.3vw) — đây là điểm tinh tế: body text chỉ nên co giãn nhẹ nhàng, từ 16px lên tối đa 19.2px, đủ để dễ chịu hơn trên màn lớn mà không phá vỡ số ký tự mỗi dòng. Bài học rút ra: heading có thể co giãn mạnh (biên độ vw lớn) để tạo ấn tượng, nhưng body text phải co giãn dè dặt. Áp cùng một độ dốc cho cả heading lẫn body là sai lầm phổ biến — mỗi vai trò văn bản cần một biên độ riêng.

Hướng dẫn từng bước

Bước 1 — Xác định cặp giá trị min và max cho từng cấp văn bản. Nhìn vào design của bạn (thường từ Figma), ghi lại: tiêu đề lớn nhất muốn bao nhiêu px ở mobile và bao nhiêu ở desktop. Ví dụ H1: 28px → 48px. H2: 22px → 32px. Body: 16px → 18px. Đây là dữ liệu đầu vào.

Bước 2 — Quyết định hai mốc viewport tham chiếu. Chọn một viewport nhỏ (ví dụ 360px hoặc 375px — đại diện cho điện thoại phổ biến ở Việt Nam) và một viewport lớn (ví dụ 1280px hoặc 1440px — desktop). Chữ sẽ chạm min tại viewport nhỏ và chạm max tại viewport lớn.

Bước 3 — Tính phần preferred, hoặc dùng công cụ. Dùng công thức slope và intercept ở phần trên, hoặc tốt hơn là vào utopia.fyi/type/calculator. Bạn nhập min viewport, max viewport, min font size, max font size — công cụ trả về ngay dòng clamp() hoàn chỉnh. Đây là cách tôi khuyên dùng cho người mới: nhanh, chính xác, không sai số học.

Bước 4 — Áp dụng vào CSS, ưu tiên dùng CSS variables. Thay vì rải clamp() khắp nơi, hãy gom vào biến để dễ bảo trì:

:root {
  --fs-h1: clamp(1.75rem, 1.2rem + 2.4vw, 3rem);
  --fs-h2: clamp(1.375rem, 1.1rem + 1.2vw, 2rem);
  --fs-body: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
}

h1 { font-size: var(--fs-h1); } h2 { font-size: var(--fs-h2); } body { font-size: var(--fs-body); }

Bước 5 — Kiểm thử bằng cách kéo giãn cửa sổ trình duyệt. Mở DevTools, vào chế độ responsive, kéo chiều rộng từ 320px lên 1920px và quan sát chữ co giãn liền mạch, không nhảy bậc. Kiểm tra hai đầu mút: ở rất nhỏ chữ không vỡ layout, ở rất lớn chữ không khổng lồ.

Bước 6 — Kiểm tra accessibility bằng zoom. Vào cài đặt trình duyệt, zoom lên 200%. Vì bạn đã dùng rem trong phần preferred, chữ phải phóng to theo. Nếu chữ không nhúc nhích khi zoom, nghĩa là bạn lỡ dùng vw thuần — quay lại sửa.

Lỗi thường gặp & mẹo

Lỗi 1 — Dùng vw thuần, phá hỏng khả năng zoom. font-size: 4vw trông fluid nhưng người dùng khiếm thị không thể phóng to chữ, vi phạm WCAG. Luôn dùng clamp() với phần preferred chứa rem.

Lỗi 2 — Đặt MIN lớn hơn MAX. clamp(3rem, ..., 1.5rem) là vô nghĩa. Nhớ thứ tự: nhỏ — vừa — lớn. Trình duyệt sẽ ưu tiên MIN nếu bị đảo, gây kết quả khó hiểu.

Lỗi 3 — Cho heading và body cùng độ dốc vw. Heading cần biên độ vw rộng để tạo ấn tượng; body cần biên độ rất hẹp để giữ độ đọc ổn định. Đừng copy y nguyên một công thức cho mọi cấp.

Lỗi 4 — Quên rằng vw dựa trên viewport, không phải container. Nếu chữ nằm trong một sidebar hẹp nhưng viewport rộng, vw vẫn tính theo viewport nên có thể quá to. Trong tương lai gần, đơn vị cqi (container query inline) sẽ giải quyết — nhưng hiện tại hãy lưu ý hạn chế này.

Mẹo — Dùng px hay rem trong clamp đều được, nhưng nhất quán. Phần MIN và MAX bạn có thể viết bằng rem để tôn trọng cỡ chữ gốc người dùng. Trộn lẫn px và rem trong cùng dự án sẽ khó bảo trì.

Mẹo — Đừng fluid hóa mọi thứ. Caption nhỏ, nhãn nút, badge thường nên giữ cố định. Fluid typography phát huy giá trị nhất ở heading và body — những phần chiếm nhiều không gian thị giác.

Bài tập thực hành

  • Tính tay một lần cho thấm. Cho H1 muốn đạt 30px tại viewport 375px và 56px tại viewport 1440px. Tính slope và intercept, viết ra dòng clamp() hoàn chỉnh (đặt MIN = 30px, MAX = 56px quy ra rem). So sánh kết quả của bạn với output từ utopia.fyi.
  • Xây một type scale fluid hoàn chỉnh. Tạo một file CSS với các biến --fs-h1 đến --fs-h4--fs-body, tất cả dùng clamp(). Yêu cầu: heading co giãn mạnh, body co giãn nhẹ. Áp lên một trang HTML đơn giản và kéo giãn trình duyệt để kiểm tra.
  • Săn lỗi vw thuần. Lấy đoạn font-size: 6vw và chuyển thành clamp() có sàn và trần hợp lý. Sau đó zoom trình duyệt 200% và xác nhận chữ phóng to đúng.
  • Tình huống thị trường VN. Mô phỏng một hero heading cho trang sale, kiểm thử ở các viewport 360px, 414px, 768px và 1280px. Ghi lại cỡ chữ thực tế tại mỗi mốc và đánh giá xem nó có cân đối không. Điều chỉnh min/max nếu thấy chữ ở tablet bị lọt thỏm.

Tóm tắt

Fluid typography giải quyết vấn đề kinh điển: chữ nhảy bậc thang giữa các breakpoint cố định. Thay vì gán cứng nhiều cỡ chữ qua media query, ta dùng hàm clamp(MIN, PREFERRED, MAX) để chữ co giãn liên tục và mượt mà theo viewport. Phần MIN bảo vệ mobile khỏi chữ quá nhỏ, MAX bảo vệ desktop khỏi chữ quá to, còn PREFERRED — luôn có dạng phần-rem + phần-vw — tạo ra sự co giãn đồng thời giữ được khả năng zoom phục vụ accessibility.

Ba điều cần khắc cốt: (1) Đừng bao giờ dùng vw thuần cho font-size, luôn bọc trong clamp(). (2) Heading co giãn mạnh, body co giãn nhẹ — mỗi cấp văn bản một biên độ. (3) Dùng công cụ như Utopia để tính chính xác, gom vào CSS variables để dễ bảo trì, và luôn kiểm thử bằng cách kéo giãn cửa sổ lẫn zoom trình duyệt. Với một thị trường thiết bị phân mảnh như Việt Nam, đây chính là kỹ thuật giúp typography của bạn luôn cân đối trên mọi màn hình mà chỉ tốn một dòng CSS.