UX — desktop đủ ý, mobile đủ dùng
Mobile là nén nội dung chứ không phải thu nhỏ desktop. Mười luật kiểm được bằng số, cộng cách đo chữ bẻ dòng và vùng chạm.
Đây là ô vàng đầu tiên bạn phải tự làm (phân vai). AI dựng giao diện rất nhanh, nhưng nó không cầm điện thoại, không đứng ngoài nắng, không có ngón tay.
Nguyên tắc gốc
Desktop đủ ý. Mobile đủ dùng.
Không phải "mobile là bản thu nhỏ của desktop". Trên điện thoại bạn bỏ bớt, gộp lại, đổi thứ tự — giữ đúng việc người ta cần làm khi đang di chuyển.
Ví dụ cụ thể, bảng đơn hàng 9 cột:
- Desktop: cả 9 cột, bảng, sắp xếp được.
- Mobile sai: vẫn 9 cột, chữ 11px, cuộn ngang. Không ai dùng.
- Mobile đúng: mỗi đơn là một thẻ 3 dòng — tên khách + tiền ở dòng đầu, trạng thái + giờ ở dòng hai, nút chính ở dòng ba. Sáu cột kia nằm trong màn chi tiết.
Mười luật kiểm được bằng số
| # | Luật | Kiểm thế nào |
|---|---|---|
| 1 | Vùng chạm tối thiểu 44×44px | Đo bằng getBoundingClientRect() |
| 2 | Chữ nội dung tối thiểu 16px trên mobile | Dưới 16px thì iOS tự phóng to khi bấm vào ô nhập |
| 3 | Không cuộn ngang | scrollWidth <= clientWidth |
| 4 | Nhãn nút không bẻ dòng | Đếm số hình chữ nhật của nút văn bản |
| 5 | Tương phản chữ/nền ≥ 4,5:1 | Công cụ kiểm tương phản |
| 6 | Bấm là biết đã bấm trong 100ms | Nút đổi trạng thái / hiện vòng chờ |
| 7 | Trạng thái rỗng nói rõ phải làm gì | Không để màn hình trắng |
| 8 | Lỗi nói đúng nguyên nhân + đường thoát | Không "Có lỗi xảy ra" |
| 9 | Việc chính làm xong trong ≤ 3 chạm | Đếm tay |
| 10 | Không sai chính tả | Đọc lại, sai chính tả là hỏng |
Luật 10 nghe nhẹ nhất nhưng là luật khách để ý nhất. Một chữ sai trên màn hình chính làm hỏng cảm giác tin cậy của cả app, bất kể bên trong tốt đến đâu.
Đo chữ bẻ dòng cho đúng
Nhãn nút tiếng Việt dài hơn tiếng Anh khoảng 30%. "Xuất báo cáo tháng" tràn chỗ mà "Export" vừa khít. Nhưng đừng đo bằng chiều cao phần tử — nút có chữ căn giữa luôn cao hơn chữ, và bạn sẽ báo nhầm hàng loạt.
Cách đúng: đếm số hình chữ nhật của nút văn bản:
// Dán vào Console — liệt kê mọi chỗ chữ bị bẻ dòng
(() => {
const w = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const out = [];
let n;
while ((n = w.nextNode())) {
const t = n.textContent.trim();
if (!t) continue;
const r = document.createRange();
r.selectNodeContents(n);
const rects = r.getClientRects();
if (rects.length > 1) {
const el = n.parentElement;
if (/^(button|a|label|th)$/i.test(el.tagName) || el.closest("button,a,label"))
out.push({ chu: t.slice(0, 40), dong: rects.length, tag: el.tagName });
}
}
return out;
})()
Và kiểm vùng chạm + cuộn ngang:
// Nút quá nhỏ cho ngón tay
[...document.querySelectorAll('button,a,input[type="checkbox"],select')]
.map(e => ({ e, r: e.getBoundingClientRect() }))
.filter(o => o.r.width > 0 && (o.r.width < 44 || o.r.height < 44))
.map(o => (o.e.textContent.trim() || o.e.tagName) + ` ${Math.round(o.r.width)}x${Math.round(o.r.height)}`)
// Phần tử chọc ra ngoài màn hình
[...document.querySelectorAll('*')]
.filter(e => e.getBoundingClientRect().right > innerWidth + 1)
.map(e => e.tagName + '.' + e.className)
Khung xem thử trong trình duyệt hoặc trong công cụ của AI thường chỉ rộng 700–1000px, trong khi màn hình làm việc thật là 1440–1920px. Nhiều lỗi bố cục chỉ hiện ra ở khổ rộng: bảng dàn ra quá thưa, chữ dài hết một dòng 1600px (rất khó đọc), ảnh kéo giãn vỡ nét.
Luật: test mobile ở 375px, test desktop ở 1280px và 1440px, ép khổ tường minh chứ đừng tin khung mặc định.
Năm thứ hỏng riêng trên điện thoại
1. Bàn phím ảo che nút Lưu. Người dùng gõ xong, không thấy nút đâu. Cách chữa: nút chính dán đáy màn hình với position:sticky; bottom:0, hoặc cuộn phần tử đang nhập vào giữa khi bàn phím mở.
2. Chọn sai kiểu bàn phím. Ô số điện thoại mà bật bàn phím chữ là bắt người ta thao tác thừa. Dùng đúng kiểu ô nhập:
<input type="tel" inputmode="numeric" autocomplete="tel">
<input type="email" inputmode="email" autocomplete="email">
<input type="text" inputmode="decimal"> <!-- tiền -->
3. iOS tự phóng to khi chạm ô nhập. Nguyên nhân duy nhất: cỡ chữ của ô nhập dưới 16px. Đặt font-size:16px cho mọi input, select, textarea.
4. Bấm hai lần ra hai bản ghi. Mạng chậm, người dùng tưởng chưa ăn, bấm lại. Phải khóa nút ngay khi bấm:
btn.disabled = true;
btn.textContent = "Đang lưu…";
try { await luu(); } finally { btn.disabled = false; btn.textContent = "Lưu"; }
Và vẫn phải có khóa chống trùng ở máy chủ — khóa nút là trải nghiệm, ràng buộc UNIQUE mới là bảo đảm.
5. Máy cũ, Safari đời cũ. iPhone 7/8 còn rất nhiều trong công ty Việt Nam. Cú pháp JavaScript quá mới làm trang trắng hoàn toàn, không báo gì. Nếu người dùng của bạn dùng máy hiện trường, hãy thử trên một máy cũ thật trước khi giao.
Trạng thái rỗng, đang tải, lỗi — ba màn hình hay bị quên
Người mới chỉ thiết kế màn hình "có dữ liệu và mọi thứ ổn". Ba màn hình còn lại là nơi người dùng bỏ cuộc.
RỖNG: "Chưa có đơn nào trong tháng 10."
[Tạo đơn đầu tiên] ← phải có đường đi tiếp
ĐANG TẢI: khung xám đúng hình dạng nội dung sắp hiện
(đừng dùng vòng xoay giữa màn hình trắng)
LỖI: "Không tải được danh sách đơn. Kiểm tra mạng rồi thử lại."
[Thử lại] ← phải có đường thoát
Luật về thông báo lỗi: nói nguyên nhân người dùng hiểu được, và cho một việc để làm. "Có lỗi xảy ra" vi phạm cả hai vế. "Đã hết phiên đăng nhập, vui lòng đăng nhập lại" thì đúng cả hai.
Tổng hợp phản hồi thật từ các app nội bộ tôi vận hành:
- "Chậm quá" — thường không phải app chậm, mà là không có phản hồi khi bấm. Thêm vòng chờ ngay trên nút là phàn nàn giảm hẳn, dù thời gian không đổi.
- "Bấm không được" — nút nhỏ hơn ngón tay, hoặc bị bàn phím che.
- "Mất dữ liệu em vừa nhập" — chuyển màn hình làm mất nội dung đang gõ. Lưu nháp vào
localStoragelà xong. - "Không biết phải làm gì" — màn hình rỗng không hướng dẫn.
- "Sao cái này lại ở đây" — thứ tự các mục không giống thứ tự họ làm việc ngoài đời.
Bốn trên năm mục không liên quan gì tới tính năng. Đây là lý do UX là ô vàng: AI làm xong tính năng rồi, phần còn lại là phần bạn phải ngồi với người dùng.
Hiệu ứng đẹp — của chủ web, không phải của bạn
Hiệu ứng mượt làm app sang hơn, nhưng cũng làm điểm tốc độ xấu đi. Đừng tự ý bỏ hiệu ứng để lấy điểm, và cũng đừng giấu cái giá.
Cách làm đúng: nêu giá bằng số rồi để chủ web chọn.
"Giữ vòng lặp chữ chạy ở đầu trang thì điểm Speed Index là 4,0 giây thay vì 2,4 giây — mất khoảng 2–3 điểm trên 100. Anh muốn giữ không?"
Chi tiết kỹ thuật ở Tốc độ web. Hai điều cần nhớ ngay:
- Hiệu ứng mờ dần ở màn hình đầu đẩy lùi phép đo tốc độ — trình duyệt không tính chữ đang trong suốt là "đã vẽ". Cho trượt (
transform) thì vô hại. - Vùng đổi pixel liên tục (chữ gõ lặp, con trỏ nhấp nháy, số đếm) làm trang "không bao giờ đứng yên" theo cách đo của máy.
Tôi không biết gọi tên, nhưng nhìn là thấy: nền tím chuyển sắc, bóng đổ dày, viền trái màu bo tròn trước mỗi khối chữ, biểu tượng tròn to ở mọi mục. Trông "hiện đại" trong 5 giây đầu rồi thành mệt mắt.
Cách tôi chữa, rất thô mà hiệu quả: nền trắng, chữ đen, đúng một màu nhấn, ít chữ. Và một luật tôi đặt ra sau khi nhìn nhiều app: không đặt nền phía dưới chữ — không quầng trắng, không hộp mờ sau tiêu đề. Chữ đứng thẳng trên nền sạch luôn dễ đọc hơn.
Khi ra đề cho AI, tôi viết thẳng những cái cấm: "Không gradient, không bóng đổ, không viền trái màu, không biểu tượng tròn. Nền trắng, một màu nhấn duy nhất."
Dựng màn hình "Nhập đơn nhanh" cho app.
Người dùng: nhân viên giao hàng, đang đứng ngoài đường, MỘT TAY cầm
điện thoại, tay kia cầm hàng. Mạng 4G chập chờn. Điện thoại Android cũ.
Ràng buộc:
- Tối đa 4 ô nhập. Việc chính xong trong 3 chạm.
- Ô Số lượng dùng nút +/−, không bắt mở bàn phím.
- Bàn phím đúng kiểu: tel cho số điện thoại, decimal cho tiền.
- Mọi input/select/textarea để font-size 16px (chống iOS tự phóng).
- Nút Lưu dán đáy màn hình, cao tối thiểu 48px, bấm là khóa ngay
và đổi chữ thành "Đang lưu…".
- Mất mạng giữa chừng: giữ nội dung đang nhập, hiện nút Thử lại.
- Có đủ 3 trạng thái: rỗng, đang tải, lỗi — mỗi cái có đường đi tiếp.
- Dùng đúng màu và cỡ chữ đang có trong app. Không thêm màu mới,
không gradient, không bóng đổ.
Xong rồi tự kiểm ở khổ 375px: liệt kê mọi nút dưới 44px và mọi chữ
bị bẻ dòng, dùng đoạn JS đo bằng getBoundingClientRect.
- Đủ: nguyên tắc gốc, 10 luật kiểm được bằng số, mã đo chữ bẻ dòng và vùng chạm, 5 lỗi riêng của điện thoại, ba màn hình hay quên, ranh giới về hiệu ứng.
- Đã trả giá thật: đo bằng
getClientRects()của nút văn bản thay vì chiều cao phần tử; bẫy khung "desktop" hẹp. - Chưa nói: trợ năng sâu (đọc màn hình, điều hướng bằng bàn phím đầy đủ), đa ngôn ngữ. Nên làm, nhưng không phải việc của Cấp 3.
- Rủi ro còn lại: mọi phép đo ở đây chạy trên máy bạn. Chúng không thay được việc đưa điện thoại cho một người dùng thật và nhìn họ bấm trong 15 phút. Đó vẫn là phép thử mạnh nhất.
- Mở app của bạn ở khổ 375px, chạy cả ba đoạn JS đo (chữ bẻ dòng, nút nhỏ, cuộn ngang). Sửa hết.
- Mở ở 1440px, kiểm bố cục không dàn quá thưa và dòng chữ không dài quá ~75 ký tự.
- Làm đủ ba màn hình rỗng / đang tải / lỗi cho màn hình chính.
- Đưa điện thoại cho một người chưa từng dùng app, giao đúng một việc, không nói gì trong 5 phút. Ghi lại mọi chỗ họ ngập ngừng.
Việc 4 sẽ cho bạn nhiều hơn ba việc trên cộng lại. Đó cũng là việc duy nhất trong bài mà AI không làm thay được.