Responsive Design: Thiết kế linh hoạt trong thế giới đa thiết bị
7/19/2026 · 16p đọc
title: "Responsive Design: Thiết kế linh hoạt trong thế giới đa thiết bị"
series: "UX Architect: 40 bài Expert-Deep Dive"
part: "Phần 3 — UI Design System & Tính nhất quán"
order: 26
audience: "Product Designer & UX Architect"
reading_time: "13 phút"
tags: ["responsive design", "mobile-first", "breakpoint", "design tokens", "frontend architecture", "adaptive layout"]
Responsive Design: Thiết kế linh hoạt trong thế giới đa thiết bị
Một designer nhận task "làm responsive cho trang báo cáo doanh thu" — màn hình có một bảng dữ liệu 9 cột: tên khách hàng, gói dịch vụ, MRR, ngày gia hạn, trạng thái, nhân viên phụ trách, ghi chú, hành động, và tag ưu tiên. Cách làm nhanh nhất: mở Figma, resize frame từ 1440px xuống 375px, để layout tự co lại, thu nhỏ font, giảm padding. Xong, tick "đã responsive".
Kết quả khi lên production: trên điện thoại, bảng 9 cột bị nén đến mức mỗi cột chỉ còn vài ký tự, chữ tràn ra ngoài ô, người dùng phải zoom ra để đọc rồi zoom vào để bấm nút — thao tác không ai muốn lặp lại lần thứ hai. Đây chính là sai lầm cốt lõi mà bài này mổ xẻ: nhầm lẫn giữa "responsive" (co giãn được) và "thiết kế lại cho đúng ngữ cảnh sử dụng" (re-architected cho từng loại thiết bị). Hai việc nghe giống nhau nhưng đòi hỏi tư duy, framework, và cả kiến trúc code hoàn toàn khác nhau.
Bài này đi sâu vào lý do responsive design không phải là bài toán CSS co giãn tỷ lệ, mà là bài toán kiến trúc: từ tư duy Mobile-First, hệ thống breakpoint có chủ đích, cho tới hệ quả trực tiếp lên cách component được tổ chức trong codebase. Đây là hạ tầng quyết định sản phẩm SaaS của bạn có dùng được thật sự trên mọi thiết bị, hay chỉ "trông không vỡ" khi demo trên máy tính.
The UX Problem
Ngộ nhận phổ biến nhất về responsive design: "responsive" nghĩa là layout tự co giãn (fluid) để vừa với mọi kích thước màn hình, và việc của designer chỉ là kiểm tra layout desktop không bị vỡ khi thu nhỏ. Ngộ nhận này bắt nguồn từ chính từ "responsive" (đáp ứng, phản hồi) — nghe như một hành vi thụ động, tự động, không cần thiết kế lại gì cả.
Vấn đề là ba loại thiết bị chính — desktop, tablet, mobile — không chỉ khác nhau về kích thước màn hình. Chúng khác nhau về ngữ cảnh sử dụng (context of use):
- Desktop: người dùng ngồi cố định, có bàn phím + chuột, màn hình rộng cho phép hiển thị nhiều thông tin song song, thường dùng cho tác vụ sâu (deep work) — phân tích báo cáo, nhập liệu hàng loạt, so sánh nhiều cột dữ liệu.
- Tablet: có thể cầm tay hoặc đặt trên giá, tương tác bằng chạm (touch) hoặc bút, thường dùng cho tác vụ trình diễn hoặc xem lướt — không phải nhập liệu phức tạp.
- Mobile: gần như luôn tương tác bằng một tay, trong ngữ cảnh di chuyển hoặc gián đoạn liên tục (đang đi, đang chờ, có thông báo khác xen vào), màn hình hẹp buộc phải ưu tiên khắt khe: chỉ hiển thị được thông tin quan trọng nhất tại một thời điểm.
Khi designer chỉ "co giãn" layout desktop, họ đang giữ nguyên logic thông tin được thiết kế cho ngữ cảnh "ngồi bàn, nhiều thông tin song song" và ép nó vào ngữ cảnh "một tay, gián đoạn liên tục, ưu tiên khắt khe". Hệ quả điển hình:
- Bảng dữ liệu nhiều cột bị nén đến mức không đọc được — mỗi cột chỉ còn vài pixel, buộc người dùng phải scroll ngang hoặc zoom, cả hai đều là trải nghiệm tệ trên touch.
- Nút bấm bị thu nhỏ dưới ngưỡng chạm được bằng ngón tay (thường cần tối thiểu ~44x44px theo các hướng dẫn accessibility phổ biến), dẫn đến bấm nhầm liên tục.
- Thông tin phụ (metadata, ghi chú, badge trạng thái) vẫn hiển thị đầy đủ như desktop, chiếm hết không gian màn hình nhỏ, đẩy hành động chính (call-to-action) xuống dưới màn hình đầu tiên.
- Navigation nhiều cấp thiết kế cho sidebar rộng bị nhồi vào một menu hamburger, làm mất hoàn toàn khả năng người dùng nhìn thấy toàn bộ cấu trúc thông tin cùng lúc.
Gốc rễ: responsive design đúng nghĩa không phải là một phép biến đổi hình học (scale layout theo tỷ lệ), mà là thiết kế lại nội dung ưu tiên gì, ẩn gì, và trình bày dưới hình thức nào cho từng ngữ cảnh sử dụng — đôi khi kết quả là một layout hoàn toàn khác, không phải bản thu nhỏ của layout gốc.
Design Framework
Có hai tư duy tiếp cận trái ngược nhau khi bắt đầu thiết kế responsive, và lựa chọn giữa chúng ảnh hưởng tới toàn bộ quá trình thiết kế lẫn code sau này.
Desktop-First: bắt đầu thiết kế màn hình rộng nhất (thường 1440px hoặc 1920px), sau đó "co lại" dần cho tablet rồi mobile. Đây là cách làm truyền thống, tự nhiên vì màn hình rộng "dễ vẽ" hơn — có nhiều không gian để nhét mọi thứ designer muốn. Nhược điểm chí mạng: khi thiết kế trên không gian rộng, designer có xu hướng thêm ngày càng nhiều yếu tố (thêm cột, thêm filter, thêm thông tin phụ) vì "còn chỗ mà". Đến khi co về mobile, phải cắt bỏ dần — và việc cắt bỏ luôn khó hơn nhiều so với việc thêm vào, dẫn đến version mobile bị chắp vá, ưu tiên thông tin không rõ ràng.
Mobile-First: bắt đầu thiết kế từ ràng buộc chặt nhất — màn hình hẹp nhất, băng thông kém nhất, ngữ cảnh sử dụng gián đoạn nhất — rồi MỞ RỘNG dần lên tablet, desktop khi có thêm không gian. Triết lý cốt lõi: nếu bạn buộc phải quyết định "trong không gian 375px này, thứ gì thực sự quan trọng nhất phải hiển thị trước", bạn sẽ tự động loại bỏ những yếu tố không thiết yếu ngay từ đầu — thay vì phải cắt bớt sau này. Khi có thêm không gian ở tablet/desktop, bạn CỘNG THÊM (progressive enhancement) — hiển thị thêm cột phụ, thêm action nhanh, thêm preview — chứ không phải trừ bớt.
Mobile-First không chỉ là quy trình thiết kế, nó còn là triết lý kỹ thuật đứng sau CSS hiện đại: các framework CSS phổ biến (bao gồm Tailwind CSS mà team FE dự án này đang dùng) mặc định style không có prefix áp dụng cho màn hình nhỏ nhất, còn các prefix breakpoint (md:, lg:, xl:) áp dụng override khi màn hình LỚN HƠN ngưỡng đó — đúng theo hướng "mở rộng dần từ ràng buộc chặt nhất", không phải hướng ngược lại.
Hệ thống breakpoint có chủ đích: đây là phần hay bị làm sai nhất. Cách làm sai phổ biến: chọn breakpoint theo kích thước thiết bị phổ biến trên thị trường (ví dụ đúng bằng độ rộng màn hình một dòng điện thoại cụ thể). Vấn đề: thị trường thiết bị thay đổi liên tục, và quan trọng hơn — breakpoint theo thiết bị không liên quan gì đến việc NỘI DUNG của bạn thực sự "vỡ" ở đâu.
Cách làm đúng: xác định breakpoint theo điểm nội dung tự nhiên bắt đầu vỡ (content-driven breakpoint). Quy trình thực tế: lấy component/layout cụ thể (ví dụ bảng dữ liệu 9 cột), kéo giãn/thu hẹp trình duyệt liên tục và quan sát chính xác độ rộng nào bắt đầu xảy ra: chữ bị wrap xấu, cột bị đè lên nhau, khoảng trắng mất cân đối, nút bấm bị dồn cục. Ghi lại độ rộng đó — đó là breakpoint thật sự cần cho nội dung này, không phải một con số tra bảng "tablet thường là 768px" một cách máy móc.
Trong thực hành, tổ chức vẫn cần một bộ breakpoint CHUẨN HOÁ dùng chung toàn hệ thống (không phải mỗi component tự chọn breakpoint riêng, sẽ dẫn đến hỗn loạn) — bộ chuẩn này được hiệu chỉnh dựa trên việc khảo sát điểm vỡ của các layout phức tạp nhất trong sản phẩm (thường là bảng dữ liệu, dashboard nhiều panel, form nhiều cột), rồi chốt thành một thang giá trị hữu hạn: ví dụ sm / md / lg / xl / 2xl. Đây chính là cầu nối dẫn tới phần kiến trúc quan trọng nhất của bài này.
The "Architect's" Insight
Đây là nơi responsive design ngừng là chuyện "layout co giãn đẹp" và trở thành một quyết định kiến trúc ảnh hưởng trực tiếp tới codebase. Nguyên lý cốt lõi: breakpoint phải được định nghĩa như một design token dùng chung giữa công cụ thiết kế và code — không phải hai bộ giá trị độc lập do hai bên tự đặt.
Hãy hình dung tình huống rất thường gặp và nguy hiểm: designer test responsive trong Figma bằng cách resize frame theo cảm tính — thu nhỏ tới khi "nhìn ổn" thì dừng, giả sử ở mốc 780px cho breakpoint tablet. Trong khi đó, codebase FE đã có sẵn cấu hình Tailwind với breakpoint md: 768px. Hai con số này (780px thiết kế vs 768px code) chênh nhau 12px — tưởng như không đáng kể. Nhưng ở đúng dải 768px-780px, đây là một "vùng mù" (dead zone): bản thiết kế Figma CHƯA TỪNG được kiểm tra ở độ rộng này (vì designer resize nhảy cóc, không dừng đúng 768px), còn code lại kích hoạt layout md: chính xác tại 768px. Kết quả: có một dải độ rộng màn hình thật (rất phổ biến ở các tablet và cửa sổ trình duyệt thu nhỏ) mà giao diện production hiển thị một trạng thái layout không ai từng nhìn thấy trong Figma, không ai review, không ai biết nó trông thế nào cho tới khi người dùng report bug.
Vấn đề lan rộng hơn khi có nhiều designer trong team, mỗi người tự ước lượng breakpoint theo cảm tính khác nhau cho từng màn hình — kết quả là hàng chục "ngưỡng vỡ" rải rác không trùng khớp với bộ breakpoint thật trong code, và QA/dev không có cách nào biết nên test ở độ rộng nào để khớp với ý đồ thiết kế.
Kiến trúc đúng — breakpoint như một token dùng chung:
breakpoints.json (nguồn sự thật duy nhất, version-controlled)
{
"sm": 640,
"md": 768,
"lg": 1024,
"xl": 1280,
"2xl": 1536
}
├── xuất ra Figma (breakpoint variable / frame preset đúng các mốc này)
└── xuất ra tailwind.config theme.screens (hoặc CSS custom media tương đương)
Hệ quả kỹ thuật cụ thể khi áp dụng đúng kiến trúc này lan xuống nhiều tầng hệ thống:
- Ở tầng công cụ thiết kế: Figma frame preset cho responsive testing phải được tạo đúng bằng các con số breakpoint thật (640/768/1024/1280/1536), không phải preset mặc định của công cụ hay số tròn cảm tính. Designer review tại đúng các mốc này, và quan trọng hơn — phải review CẢ hai phía của mỗi mốc (ví dụ 767px và 768px) vì đó là nơi component chuyển đổi trạng thái đột ngột, dễ sinh lỗi giao diện nhất.
- Ở tầng codebase FE: breakpoint không nên hardcode rải rác trong từng component (
@media (min-width: 780px)xuất hiện ở một file,@media (min-width: 800px)ở file khác do các dev khác nhau tự đoán), mà phải tham chiếu về một cấu hình trung tâm (theme.screenstrong cấu hình Tailwind của dự án, hoặc CSS custom properties tương đương). Điều này đảm bảo MỌI component trong hệ thống chuyển layout tại cùng một điểm, không lệch nhau vài pixel giữa các phần khác nhau của cùng một sản phẩm. - Ở tầng component architecture: khi một layout thay đổi cấu trúc hoàn toàn giữa các breakpoint (không chỉ co giãn kích thước mà đổi hẳn cách trình bày — ví dụ bảng thành thẻ, xem phần Practical Example bên dưới), đây không còn là vấn đề CSS thuần tuý mà là vấn đề kiến trúc component. Team cần quyết định rõ: dùng MỘT component với logic điều kiện render theo breakpoint (
useMediaQueryhook hoặc CSS-only ẩn/hiện bằng class), hay dùng HAI component riêng biệt (DataTablecho desktop,DataCardListcho mobile) được chọn render ở tầng cha dựa trên breakpoint hiện tại. Lựa chọn này ảnh hưởng trực tiếp tới việc test coverage, bundle size (ẩn bằng CSS vẫn tải cả hai DOM tree, còn tách component có thể lazy-load riêng), và độ phức tạp khi maintain về sau. - Ở tầng dữ liệu/API khi layout khác nhau cần dữ liệu khác nhau: một hệ quả kiến trúc ít ai nghĩ tới — nếu bảng desktop hiển thị 9 cột nhưng card mobile chỉ hiển thị 4 trường quan trọng nhất, có nên để FE gọi API lấy đủ 9 trường rồi tự ẩn bớt trên mobile (đơn giản nhưng lãng phí băng thông, quan trọng với ngữ cảnh mobile vốn thường có mạng kém hơn), hay thiết kế API trả về payload khác nhau tuỳ thiết bị/breakpoint (tối ưu băng thông nhưng thêm độ phức tạp cho tầng backend và caching)? Đây là quyết định UX (chọn hiển thị 4 trường nào là quan trọng nhất trên mobile) trực tiếp kéo theo một quyết định kiến trúc API — không phải chuyện chỉ nằm ở CSS.
- Ở tầng testing/CI: bộ breakpoint chuẩn hoá cho phép viết test responsive tự động (visual regression test) tại đúng các mốc đã thống nhất, thay vì test viên tự chọn độ rộng ngẫu nhiên mỗi lần — đảm bảo mọi lần release đều tái kiểm tra đúng các điểm chuyển đổi quan trọng.
Nói cách khác: quyết định "breakpoint là bao nhiêu, ai định nghĩa, đồng bộ giữa design và code bằng cách nào" không phải là chi tiết kỹ thuật vặt vãnh — nó là quyết định kiến trúc dữ liệu tương tự việc chọn single source of truth cho color token đã bàn ở bài Design System. Một UX Architect giỏi cần chủ động đề xuất breakpoint như một token chính thức trong hệ thống, thay vì để mỗi bên (designer và dev) tự đoán một con số riêng rồi hy vọng chúng trùng nhau.
Practical Example
Tình huống cụ thể: màn hình "Danh sách khách hàng" trong CRM hiển thị bảng 9 cột trên desktop — Tên khách hàng, Gói dịch vụ, MRR, Ngày gia hạn, Trạng thái, Nhân viên phụ trách, Ghi chú, Tag ưu tiên, Hành động.
Bước 1 — Xác định ưu tiên thông tin theo ngữ cảnh sử dụng mobile. Trên mobile, người dùng thường mở màn hình này trong ngữ cảnh: đang di chuyển, cần tra nhanh 1-2 khách hàng cụ thể, không phải phân tích so sánh hàng loạt. Do đó 4 trường quan trọng nhất là: Tên khách hàng, MRR, Trạng thái, Ngày gia hạn. Các trường còn lại (Ghi chú, Tag, Nhân viên phụ trách) chuyển vào màn hình chi tiết khi tap vào, không cần hiển thị ngay ở danh sách.
Bước 2 — Thiết kế lại layout, không chỉ co giãn. Bảng 9 cột trên desktop chuyển thành danh sách thẻ (card) trên mobile, mỗi thẻ trình bày 4 trường ưu tiên theo chiều dọc:
Desktop (>= lg, 1024px): <table> 9 cột, mỗi hàng 1 khách hàng
Mobile (< md, < 768px): <CustomerCard> xếp dọc, mỗi thẻ 1 khách hàng
┌─────────────────────────┐
│ Nguyễn Văn A [●Active]│
│ Gói Pro · MRR 2.500.000đ │
│ Gia hạn: 15/08/2026 │
└─────────────────────────┘
Bước 3 — Ánh xạ sang kiến trúc component. Vì đây là hai cấu trúc DOM khác nhau hoàn toàn (table vs card list), không phải cùng một component chỉ đổi CSS, team chọn kiến trúc "component cha chọn render":
// CustomerListView.tsx
function CustomerListView({ customers }: { customers: Customer[] }) {
const isDesktop = useBreakpoint('lg'); // tham chiếu breakpoints.json chung
return isDesktop
? <CustomerTable data={customers} columns={ALL_9_COLUMNS} />
: <CustomerCardList data={customers} fields={PRIORITY_4_FIELDS} />;
}
// breakpoints.ts — nguồn sự thật duy nhất, đồng bộ với Figma variable cùng tên
export const BREAKPOINTS = {
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
} as const;
Bước 4 — Quyết định về dữ liệu. Team xác nhận: chênh lệch payload giữa 9 trường và 4 trường không đáng kể về băng thông cho use case này, nên chọn phương án đơn giản hơn — API luôn trả đủ dữ liệu, FE tự chọn trường hiển thị theo breakpoint. Quyết định này được ghi lại rõ ràng (không phải mặc định không bàn tới) vì nếu sau này có trường nặng (ví dụ ảnh đại diện độ phân giải cao, lịch sử giao dịch dài), quyết định sẽ cần đổi sang API trả payload rút gọn cho mobile.
Bảng so sánh hai cách tiếp cận sai và đúng:
| Khía cạnh | Cách sai (chỉ co giãn) | Cách đúng (thiết kế lại theo ngữ cảnh) |
|---|---|---|
| Layout mobile | Bảng 9 cột bị nén nhỏ | Card list 4 trường ưu tiên |
| Component code | 1 component Table với CSS ẩn cột |
2 component: Table + CardList, chọn render theo breakpoint |
| Trải nghiệm chạm | Nút thu nhỏ dưới ngưỡng chạm được | Nút đủ lớn, mỗi thẻ là vùng chạm rõ ràng |
| Breakpoint dùng | Designer đoán cảm tính trong Figma | Tham chiếu breakpoints.json chung với code |
| Review trước khi ship | Chỉ xem ở 375px và 1440px | Review cả hai phía của mốc chuyển layout (767px/768px) |
Common Pitfalls
1. Chỉ test responsive trên 2-3 kích thước phổ biến rồi coi là đủ. Designer/QA thường chỉ kiểm tra ở đúng độ rộng của một-hai dòng thiết bị phổ biến (ví dụ đúng độ rộng một mẫu điện thoại và một mẫu máy tính bảng cụ thể), bỏ qua toàn bộ dải độ rộng ở giữa — nơi có rất nhiều thiết bị thật (máy tính bảng khác kích thước, cửa sổ trình duyệt desktop bị thu nhỏ, chế độ chia đôi màn hình) đang tồn tại. Vùng "giữa" này chính là nơi các bug layout nghiêm trọng nhất ẩn náu vì không ai từng nhìn thấy nó trước khi ship.
2. Thiết kế responsive nhưng không kiểm chứng bảng dữ liệu phức tạp có thực sự DÙNG ĐƯỢC trên màn hình nhỏ, không chỉ "không vỡ". Có một khoảng cách lớn giữa "layout không tràn ra ngoài màn hình" và "người dùng có thể hoàn thành tác vụ thật trên màn hình đó". Một bảng 9 cột "responsive" theo kiểu ẩn bớt cột hoặc cho scroll ngang có thể trông ổn trong ảnh chụp màn hình, nhưng nếu người dùng thực sự cần so sánh MRR và ngày gia hạn của 5 khách hàng cùng lúc, mà hai trường đó không bao giờ nằm trong nhóm cột hiển thị mặc định, tính năng coi như hỏng trên mobile dù kỹ thuật "không vỡ".
3. Breakpoint trong Figma và breakpoint trong code không khớp nhau, và không ai phát hiện ra cho tới khi có bug report. Đây là lỗi kiến trúc đã phân tích ở phần Architect's Insight — designer tự ước lượng breakpoint theo cảm tính thay vì tra đúng giá trị đang cấu hình trong code, tạo ra vùng mù chưa từng được kiểm tra. Lỗi này đặc biệt nguy hiểm vì nó không hiện diện trong bất kỳ bản review Figma nào — chỉ lộ ra khi người dùng thật mở đúng độ rộng màn hình rơi vào vùng mù đó.
Checklist
- Đã xác định tư duy tiếp cận rõ ràng: Mobile-First (thiết kế từ ràng buộc chặt nhất, mở rộng dần) thay vì Desktop-First rồi cắt bớt
- Bộ breakpoint được định nghĩa như MỘT nguồn sự thật duy nhất, dùng chung giữa Figma và cấu hình code (ví dụ
theme.screenstrong Tailwind), không phải hai bộ số tự đoán độc lập - Breakpoint được hiệu chỉnh dựa trên điểm nội dung thực sự "vỡ" (đã kéo giãn/thu hẹp kiểm tra trực tiếp), không chỉ tra theo kích thước thiết bị phổ biến trên thị trường
- Đã xác định rõ, cho từng màn hình phức tạp: thông tin nào là ưu tiên bắt buộc hiển thị trên mobile, thông tin nào có thể chuyển vào màn hình chi tiết
- Với layout thay đổi cấu trúc hoàn toàn giữa breakpoint (bảng thành thẻ), đã quyết định rõ kiến trúc component (một component điều kiện vs hai component tách biệt) và lý do chọn
- Đã review thiết kế tại CẢ hai phía của mỗi mốc breakpoint (ví dụ 767px và 768px), không chỉ tại các mốc kích thước thiết bị tròn trịa
- Đã kiểm chứng bằng thao tác thật (không chỉ nhìn ảnh) rằng tác vụ cốt lõi của màn hình có thể hoàn thành được trên màn hình nhỏ nhất trong phạm vi hỗ trợ
🔗 Bài viết liên quan
Bài trước: Spacing System · Bài tiếp theo: Motion Design