Mở đầu — vì sao bài này quan trọng
Nút bấm (button) là thành phần bạn thiết kế nhiều nhất trong cả sự nghiệp UI. Trên một màn hình đăng ký của app ngân hàng, một trang đặt hàng của sàn thương mại điện tử, hay một form liên hệ đơn giản — cái nút "Đăng ký", "Thanh toán", "Gửi" luôn là điểm mà toàn bộ dòng chảy tương tác đổ về. Nếu button sai, người dùng lưỡng lự, bấm nhầm, hoặc tệ hơn là không biết mình vừa bấm hay chưa.
Điều thú vị là hầu hết designer junior đều nghĩ button là thứ đơn giản nhất: một hình chữ nhật bo góc với dòng chữ ở giữa. Nhưng khi bạn bàn giao (handoff) cho engineer, họ sẽ hỏi ngay hàng loạt câu: "Nút này lúc hover trông thế nào? Lúc đang loading thì sao? Khi bị disabled? Focus bằng bàn phím thì viền ra sao? Nếu label dài quá thì nó co giãn kiểu gì?" Nếu bạn chỉ vẽ một trạng thái duy nhất, engineer sẽ phải tự đoán — và họ đoán sai.
Bài này dạy bạn nhìn button như một component có cấu trúc (anatomy) và một tập trạng thái (states) đầy đủ. Đây chính là ranh giới phân biệt một designer "vẽ đẹp" với một designer "làm được sản phẩm thật". Nắm vững nó, bạn không chỉ thiết kế nhanh hơn mà còn bàn giao sạch sẽ hơn, giảm số vòng qua lại (back-and-forth) với dev từ 5 lần xuống còn 1.
Khái niệm cốt lõi
Anatomy — giải phẫu của một button
Một button, dù trông đơn giản, được cấu thành từ những phần rất rõ ràng. Hãy hình dung theo trục ngang:
┌─────────────────────────────────────┐
│ [Icon-leading] Label [Icon-trailing] │
└─────────────────────────────────────┘
↑ ↑ ↑
icon đầu nhãn chữ icon cuối
Các thành phần:
- Container: khối nền của button — nền (fill), viền (border/stroke), bo góc (corner radius) và bóng đổ (shadow) nếu có.
- Label: phần chữ, là "linh hồn" của button. Chữ phải rõ ràng, thường là động từ hành động: "Lưu", "Đặt hàng", "Tiếp tục".
- Leading icon (icon đầu): đứng trước label, thường minh họa hành động — ví dụ icon giỏ hàng trước chữ "Thêm vào giỏ".
- Trailing icon (icon cuối): đứng sau label, thường chỉ hướng — ví dụ mũi tên "→" sau chữ "Tiếp tục".
- Padding dọc (top/bottom) nên "ôm" (hug) theo chiều cao của icon và chữ — thường 8–12px cho button cỡ trung.
- Padding ngang (left/right) nên rộng rãi hơn, thường 12–16px cho button cỡ trung, để chữ không bị "chật chội" sát mép. Một quy tắc kinh nghiệm: padding ngang thường gấp khoảng 1.5–2 lần padding dọc.
- Gap giữa icon và label thường là 8px — đủ để tách bạch nhưng vẫn thành một khối thị giác.
- Min-width / min-height: button không nên co lại quá nhỏ. Một nút chính thường có chiều cao tối thiểu 40–48px (đặc biệt trên mobile, để đạt vùng chạm tối thiểu 44px theo khuyến nghị của Apple).
- Vùng chạm (touch target): kể cả khi button trông nhỏ, vùng có thể bấm được nên tối thiểu 44×44px trên mobile để ngón tay không bấm trượt.
States — các trạng thái BẮT BUỘC phải có
Đây là phần quan trọng nhất của bài. Một button không phải một hình ảnh tĩnh, nó là một đối tượng phản hồi theo hành vi người dùng. Có 7 trạng thái bạn bắt buộc phải định nghĩa khi thiết kế nghiêm túc:
- Default (Rest / Enabled): trạng thái nghỉ, khi người dùng chưa tương tác. Đây là gốc, mọi trạng thái khác đều biến thể từ đây.
- Hover: khi con trỏ chuột di vào (chỉ tồn tại trên desktop, không có trên touch). Thường làm nền đậm hơn một chút — ví dụ giảm 8–10% độ sáng (lightness) so với default. Đây là tín hiệu "cái này bấm được đấy".
- Focus (Focused / Keyboard focus): khi button được chọn bằng phím Tab. Đây là trạng thái bị bỏ quên nhiều nhất nhưng cực kỳ quan trọng cho khả năng tiếp cận (accessibility). Thường thể hiện bằng một viền focus (focus ring) — ví dụ viền 2px màu xanh cách button 2px. Người dùng bàn phím và người khiếm thị dùng bàn phím phụ thuộc hoàn toàn vào trạng thái này.
- Active (Pressed): khoảnh khắc đang nhấn xuống. Thường làm nền đậm hơn nữa hoặc thêm hiệu ứng "lún xuống" (scale nhẹ xuống 98%, hoặc shadow giảm). Nó tạo cảm giác vật lý — người dùng "cảm" được cú bấm.
- Disabled: khi button không thể bấm (ví dụ chưa điền đủ form). Thường giảm độ mờ (opacity ~40%) hoặc chuyển sang tông xám, con trỏ đổi thành
not-allowed. Lưu ý: button disabled không cần đạt chuẩn tương phản màu, nhưng phải rõ ràng là "đang tắt".
- Loading: khi hành động đang xử lý (ví dụ đang gọi API thanh toán). Thường thay label bằng spinner, hoặc đặt spinner ở vị trí leading icon, và khóa button để tránh double-submit (bấm hai lần gửi hai đơn hàng). Đây là trạng thái then chốt trong các luồng có network.
- Selected / Toggled (tùy loại button): với button đóng vai trò công tắc (như nút filter "Đang bật/Đang tắt"), cần một trạng thái thể hiện đang được chọn.
- Primary: hành động chính, nổi bật nhất (nền đặc, màu thương hiệu).
- Secondary: hành động phụ (viền, nền nhạt/trong suốt).
- Tertiary / Ghost / Text: hành động ít quan trọng (chỉ có chữ, không nền).
- Destructive / Danger: hành động nguy hiểm (xóa, hủy) — thường màu đỏ.
Tình huống thực tế
Ví dụ 1: Tiki và cú "double-submit" mất tiền thật
Hãy tưởng tượng đội thiết kế của một sàn thương mại điện tử lớn tại Việt Nam như Tiki. Trong một sprint, họ nhận báo cáo: khoảng 0.4% đơn hàng bị tạo trùng (duplicate) — người dùng bấm "Đặt hàng", màn hình đứng vài giây vì mạng 3G yếu, họ tưởng chưa bấm được nên bấm lại, và hệ thống tạo hai đơn.
Điều tra ra, gốc rễ là button không có trạng thái Loading. Khi bấm, nút vẫn hiển thị "Đặt hàng" như thường, không có phản hồi nào. Designer đã bỏ sót state này khi thiết kế.
Giải pháp: thêm state Loading — ngay khi bấm, label đổi thành spinner xoay, button bị disabled cho tới khi server phản hồi. Chỉ một thay đổi nhỏ này, tỷ lệ đơn trùng giảm gần về 0. Bài học: trong mọi luồng có gọi network (thanh toán, đăng ký, gửi form), state Loading không phải "trang trí" mà là cơ chế bảo vệ nghiệp vụ. Thiếu nó là thiếu một mảnh logic, không phải thiếu thẩm mỹ.
Ví dụ 2: Startup fintech và trạng thái Disabled gây hiểu lầm
Một startup fintech giả định tại TP.HCM, gọi là "VíNhanh", thiết kế màn hình chuyển tiền. Nút "Xác nhận chuyển" bị làm disabled cho tới khi người dùng nhập đủ số tiền và số tài khoản. Vấn đề: họ làm disabled bằng cách chuyển nút sang màu xám nhạt gần như biến mất, và không có bất kỳ chỉ dẫn vì sao nút bị tắt.
Kết quả từ phiên user testing với 8 người dùng: 5 người nhìn nút xám, tưởng app bị lỗi, và thoát ra. Họ không hiểu rằng "nút đang chờ mình điền cho đủ".
Đội thiết kế sửa theo hai hướng: (1) giữ nút disabled nhưng thêm dòng chữ nhỏ bên dưới "Vui lòng nhập đủ thông tin để tiếp tục", và (2) khi người dùng chạm vào nút disabled, hiển thị tooltip gợi ý trường còn thiếu. Tỷ lệ hoàn thành luồng chuyển tiền tăng đáng kể. Bài học: state Disabled không được im lặng. Người dùng cần biết vì sao nút tắt và làm gì để bật nó lên. Disabled mà không kèm ngữ cảnh là một cái bẫy.
Ví dụ 3: Handoff thiếu Focus state và bài toán accessibility
Một agency thiết kế nhận dự án làm lại website cho một ngân hàng. Designer bàn giao file Figma với button rất đẹp: Default, Hover, Active đều chỉn chu. Engineer code y hệt. Mọi thứ trông ổn — cho tới khi bên kiểm thử accessibility (do ngân hàng yêu cầu tuân thủ WCAG cho khách hàng khiếm thị) chạy audit và fail: không có Focus state.
Người dùng bàn giao bằng bàn phím (Tab để di chuyển) hoàn toàn không thấy mình đang đứng ở nút nào. Với một website ngân hàng, đây là lỗi nghiêm trọng về pháp lý lẫn đạo đức.
Vì designer không định nghĩa focus ring, engineer đành tự thêm viền mặc định của trình duyệt — trông xấu và không đồng nhất với hệ thống. Cả đội phải làm lại vòng handoff. Bài học: Focus state là bắt buộc, không phải tùy chọn. Hãy định nghĩa nó ngay trong component ở Figma (dùng một style focus ring nhất quán), để engineer không phải đoán và không phải chế cháo.
Hướng dẫn từng bước
Dưới đây là quy trình dựng một button component chuẩn chỉnh trong Figma, sẵn sàng để handoff:
Bước 1 — Dựng anatomy bằng Auto Layout. Tạo một frame, bật Auto Layout theo chiều ngang. Đặt padding: ngang 16px, dọc 10px, gap 8px. Thêm ba layer con: leading icon (có thể ẩn), label (text), trailing icon (có thể ẩn). Nhờ Auto Layout, khi label dài ra button tự giãn — đúng như cách nó hoạt động trong code.
Bước 2 — Định nghĩa min-height. Đặt chiều cao tối thiểu 44px (hoặc 40px cho desktop dày đặc). Điều này đảm bảo vùng chạm đủ lớn trên mobile.
Bước 3 — Chuyển thành Component và tạo Variants. Chuyển frame thành component. Tạo các property: variant (primary/secondary/tertiary/destructive), state (default/hover/focus/active/disabled/loading), hasLeadingIcon (boolean), hasTrailingIcon (boolean), size (small/medium/large).
Bước 4 — Thiết kế từng state từ default. Bắt đầu từ default, rồi biến thể:
- Hover: giảm lightness của nền ~8%.
- Focus: thêm focus ring 2px cách nút 2px.
- Active: giảm lightness thêm, hoặc scale 98%.
- Disabled: opacity 40%, đổi con trỏ.
- Loading: ẩn label, hiện spinner.
color/button/primary/bg). Sau này đổi theme hay dark mode chỉ cần đổi token.Bước 6 — Kiểm tra tương phản. Với mọi state có chữ (default, hover, active), đảm bảo tương phản chữ/nền đạt tối thiểu 4.5:1. Riêng disabled được miễn.
Bước 7 — Ghi chú handoff. Trong Dev Mode hoặc bằng annotation, ghi rõ: token nào dùng cho state nào, hành vi loading (khóa nút, chống double-submit), và focus ring dùng chung toàn hệ thống.
Lỗi thường gặp & mẹo
Lỗi 1: Chỉ thiết kế state Default. Đây là lỗi kinh điển của junior. Handoff một button không states giống như đưa dev một nửa bản vẽ. Luôn dựng đủ 7 states.
Lỗi 2: Nhầm giữa states và variants. Primary/Secondary là vai trò (variant); Hover/Disabled là trạng thái (state). Một button Primary vẫn phải có đủ states. Đừng gộp lẫn hai trục này.
Lỗi 3: Quên Focus state. Như ví dụ 3, đây là lỗi accessibility phổ biến nhất. Mẹo: tạo một component focus ring dùng chung, chèn vào mọi button.
Lỗi 4: Disabled dùng opacity đè lên nội dung chồng lớp. Nếu button có shadow, giảm opacity toàn nút có thể làm shadow trông kỳ. Mẹo: dùng token màu disabled riêng cho nền và chữ thay vì hạ opacity mù quáng.
Lỗi 5: Padding cứng nhắc khiến label dài bị cắt. Luôn dùng Auto Layout để button tự giãn theo label, đặc biệt quan trọng với tiếng Việt vì chữ có dấu và từ thường dài hơn tiếng Anh ("Xác nhận" dài hơn "OK").
Lỗi 6: Hover trên mobile. Nhớ rằng touch device không có hover. Đừng đặt thông tin quan trọng vào state hover — nếu không, người dùng mobile sẽ không bao giờ thấy.
Mẹo vàng: Đặt tên states và variants nhất quán, khớp với tên trong code (ví dụ state=loading ↔ prop isLoading). Điều này khiến engineer đọc file Figma là hiểu ngay, giảm ma sát handoff.
Bài tập thực hành
- Dựng button component đầy đủ: Tạo một button Primary trong Figma với Auto Layout, đủ 7 states (default, hover, focus, active, disabled, loading, selected). Dùng variants để quản lý.
- Audit một app thật: Mở một app Việt bạn hay dùng (Momo, ZaloPay, Shopee). Chọn một nút chính, dùng Tab/hover thử, và ghi lại: nó có bao nhiêu states rõ ràng? Thiếu state nào? Nếu là bạn, sẽ bổ sung gì?
- Thiết kế loading chống double-submit: Vẽ luồng một nút "Thanh toán" từ default → active → loading → success/error. Ghi chú handoff cho engineer về hành vi khóa nút.
- Bài tập accessibility: Thiết kế focus ring dùng chung cho cả 4 variants, đảm bảo nó luôn nhìn rõ trên mọi nền màu (kể cả nút destructive màu đỏ).
Tóm tắt
Button không phải một hình chữ nhật có chữ — nó là một component có cấu trúc rõ ràng (container, label, leading/trailing icon, padding, gap, min-size) và một tập trạng thái đầy đủ: Default, Hover, Focus, Active, Disabled, Loading, và Selected khi cần. Bên cạnh states là trục variants theo vai trò (primary, secondary, tertiary, destructive) — hai trục này độc lập và mỗi variant phải có đủ states.
Ba trạng thái bị bỏ quên nhiều nhất mà bạn phải nhớ: Focus (cứu accessibility), Loading (chống double-submit, bảo vệ nghiệp vụ), và Disabled có ngữ cảnh (đừng để nút tắt trong im lặng). Ba ví dụ từ sàn thương mại điện tử, fintech và ngân hàng cho thấy: mỗi state thiếu sót không chỉ là lỗi thẩm mỹ mà là lỗ hổng về nghiệp vụ, trải nghiệm và cả pháp lý.
Khi dựng trong Figma, hãy dùng Auto Layout để button tự giãn, dùng variables/tokens cho màu để hỗ trợ theming, và ghi chú handoff rõ ràng khớp tên với code. Làm được vậy, bạn không chỉ vẽ đẹp — bạn giao được sản phẩm thật.