Beaket Blog
Changelog

Design System biết nói không

Design System biết nói không

“Card nên bo góc 4px hay 8px?”

Đó là một cuộc thảo luận kéo dài 47 phút. Bốn kỹ sư, hai designer, một file Figma với sáu biến thể. Cuộc thảo luận bao gồm quy chuẩn iOS, hướng dẫn Material Design, căn chỉnh quang học, và có lúc còn nhắc đến tỷ lệ vàng. Không ai sai cả. Đó chính là vấn đề.

Chúng tôi đã từng ở đây trước đó. Không chỉ với border-radius — mà còn với hướng gradient, giá trị blur của shadow, mức opacity cho trạng thái disabled. Mỗi quyết định đều nhỏ. Mỗi cuộc tranh luận đều hợp lý. Và gộp lại, chúng đang ăn mòn tốc độ phát triển của chúng tôi.

Vì vậy chúng tôi ngừng cố tìm đáp án đúng. Chúng tôi loại bỏ luôn câu hỏi.

Cái giá của các lựa chọn

Mỗi thuộc tính thị giác trong một design system là một bề mặt quyết định. Riêng border-radius đã sinh ra hàng loạt câu hỏi: Button và card có nên dùng chung giá trị không? Còn các phần tử lồng nhau thì sao — bán kính bên trong có trừ đi bán kính bên ngoài không? Bán kính có thay đổi theo kích thước không?

Không câu hỏi nào khó cả. Đó chính là điều khiến chúng nguy hiểm. Chúng đủ dễ để ai cũng có ý kiến, nhưng đủ ít quan trọng để không ý kiến nào chứng minh được là tốt hơn. Bikeshedding kinh điển.

Chúng tôi đã theo dõi thủ công các comment trong PR suốt một quý. Khoảng 30% các thread review design system là về các giá trị thẩm mỹ không ảnh hưởng đến khả năng sử dụng. Không phải layout. Không phải accessibility. Không phải interaction pattern. Chỉ là… thẩm mỹ.

Đó là lúc chúng tôi viết ra các quy tắc.

Tại sao loại bỏ thay vì giới hạn

Phương án thay thế hiển nhiên là giữ các thuộc tính nhưng giới hạn chúng. Hầu hết các design system trưởng thành đều làm vậy — ba giá trị border-radius được phép, thang shadow elevation, bảng gradient được chọn lọc. Chúng tôi đã cân nhắc.

Vấn đề là các lựa chọn bị giới hạn vẫn tạo ra tranh luận. “Card này nên dùng radius-sm hay radius-md?” vẫn là cùng một cuộc hội thoại với ít đáp án hơn. Ít thì tốt hơn nhiều, nhưng không thì tốt hơn ít. Giới hạn một thuộc tính giảm số quyết định. Loại bỏ nó xóa bỏ hoàn toàn quyết định đó.

Chúng tôi cũng xem xét dùng linting — một rule ESLint từ chối các giá trị không được phép. Nhưng một linter bắt buộc “chỉ dùng 0, 4, hoặc 8px radius” vẫn để lại ba lựa chọn. Mục tiêu không phải là làm cho quyết định dễ hơn. Mà là làm cho nó không cần thiết.

Bốn quy tắc

Thư viện component của Beaket có bốn ràng buộc thị giác:

  1. Không border-radius. Góc vuông cho mọi thứ. Button, card, input, dialog, tooltip — tất cả đều hình chữ nhật.
  2. Không gradient. Chỉ màu phẳng. Mỗi bề mặt là một CSS variable duy nhất.
  3. Không blur shadow. Không box-shadow với blur radius. Chỉ offset shadow.
  4. Không opacity trang trí. Trạng thái disabled dùng thay đổi màu sắc, không dùng độ trong suốt. Không dùng opacity: 0.5 cho tiện.

Đây không phải hướng dẫn. Đây là quy tắc cứng, được thực thi trong code review và ghi nhận trong component guidelines. Một PR thêm rounded-md sẽ bị từ chối.

Tại sao lại chọn bốn thuộc tính này? Vì chúng là những thuộc tính tạo ra nhiều tranh luận nhất với ít ảnh hưởng đến khả năng sử dụng nhất. Spacing và typography ảnh hưởng đến khả năng đọc. Color ảnh hưởng đến ý nghĩa. Nhưng sự khác biệt giữa border-radius 4px và 8px? Giữa blur shadow 4px và 6px? Chúng tôi không tìm được một trường hợp nào mà những lựa chọn đó ảnh hưởng đo lường được đến cách người dùng sử dụng sản phẩm.

Sau khi áp dụng các quy tắc, các PR cho design system bắt đầu chạy nhanh hơn. Các thread review ngắn lại. Designer ngừng tạo nhiều biến thể của cùng một component chỉ khác nhau ở cách bo góc hay độ sâu shadow. Các cuộc tranh luận không giảm — chúng biến mất.

Chúng tôi dùng gì thay thế

Loại bỏ lựa chọn không có nghĩa là loại bỏ phân cấp thị giác. Bạn vẫn cần truyền đạt độ sâu, trạng thái và tương tác. Đây là cách chúng tôi thay thế từng thuộc tính.

Offset Shadow thay cho Blur

Thay vì box-shadow: 0 4px 6px rgba(0,0,0,0.1), chúng tôi dùng offset shadow cứng được định nghĩa dưới dạng CSS variable:

--shadow-offset: 1px 1px 0px 0px var(--color-chrome);
--shadow-offset-hover: 2px 2px 0px 0px var(--color-chrome);
--shadow-offset-active: 0px 0px 0px 0px var(--color-chrome);

Ba trạng thái tương tác. Không blur. Đó là toàn bộ hệ thống shadow.

Trạng thái mặc định có offset 1px — phần tử nổi nhẹ trên bề mặt. Hover đẩy lên 2px — một sự nâng nhẹ. Active đưa về 0px — phần tử ấn phẳng xuống. Đó là một animation cơ học nhỏ, truyền đạt tương tác rõ ràng và không để lại gì cho developer phải cấu hình.

Trong thực tế, mọi interactive component đều áp dụng ba token giống nhau. Đây là phần cơ bản của button component (đã đơn giản hóa từ chuỗi CVA đầy đủ, bao gồm cả trạng thái disabled và focus):

const buttonVariants = cva(
  [
    "inline-flex items-center justify-center gap-2",
    "font-medium cursor-pointer",
    "shadow-offset hover:shadow-offset-hover active:shadow-offset-active",
    "transition-shadow duration-100",
    // ... disabled, focus-visible, and icon styles
  ].join(" "),
  // ... variants
);

Không cần quyết định component nào “xứng đáng” với mức shadow nào.

Màu phẳng thông qua CSS Variable

Bảng màu của chúng tôi — chúng tôi gọi là “Porcelain” — có 14 sắc trung tính từ graphite (#030509) đến paper (#ffffff). Chúng lạnh và hơi ngả xanh dương. Mô tả theme trong tài liệu viết: “Độ chính xác công nghiệp. Khắc trên bảng điều khiển bằng sơn mài tổng hợp đen. Trắng tinh, mực đen xanh lạnh, bóng mờ ma mị, điểm nhấn xanh ngọc.”

Mọi tham chiếu màu đều đi qua semantic token:

--color-text-primary: var(--color-graphite);
--color-text-secondary: var(--color-steel);
--color-bg-primary: var(--color-paper);
--color-border-default: var(--color-chrome);

Chúng tôi tránh dùng màu mặc định của Tailwind như bg-white hay text-gray-500. Mọi màu đều là named variable, và mọi variable đều ánh xạ đến một vai trò. Không ai tranh luận liệu thứ gì đó nên là gray-100 hay gray-200 — semantic token đã trả lời câu hỏi rồi.

Góc vuông ở khắp nơi

Cái này không cần thay thế. Bạn chỉ đơn giản… không thêm border-radius.

Điều khiến chúng tôi ngạc nhiên là góc vuông tương tác tốt đến thế nào với offset shadow. Một card bo góc với offset shadow trông kỳ lạ — cạnh cứng của shadow xung đột với góc mềm. Nhưng một card góc vuông với offset shadow trông có chủ đích. Hình học nhất quán. Các ràng buộc củng cố lẫn nhau theo cách chúng tôi không hoàn toàn lường trước.

Ngôn ngữ thị giác trở nên nhất quán nhờ phép trừ. Khi mọi thứ đều sắc cạnh, không gì cần biện minh cho hình dạng của mình.

Nơi các ràng buộc cộng hưởng

Lợi ích rõ nét nhất của cách tiếp cận này thể hiện ở các vấn đề xuyên suốt — những pattern chạm đến mọi component.

Trạng thái disabled nổi tiếng là không nhất quán giữa các thư viện component. Một số dùng opacity, một số thay đổi background, một số làm cả hai. Chúng tôi có một pattern duy nhất, áp dụng cho mọi component:

disabled:shadow-none disabled:cursor-not-allowed disabled:border-dashed disabled:border-chrome disabled:bg-frost disabled:text-steel

Button, input, select, checkbox, switch — tất cả giống hệt nhau. Shadow bị xóa (affordance tương tác biến mất), border nét đứt, background frost, text steel. Khi bạn đã thấy một component disabled, bạn đã thấy tất cả. Các ràng buộc loại bỏ sự cám dỗ phải làm trạng thái disabled “phù hợp” với tính cách thị giác của từng component. Khi component không có tính cách thị giác, trạng thái disabled tự nhiên hội tụ.

Focus ring hoạt động tương tự. Mọi phần tử có thể focus đều dùng:

focus-visible:outline-2 focus-visible:outline-signal-blue focus-visible:outline-offset-2

Một màu. Một độ dày. Một offset. Người dùng bàn phím điều hướng qua form sẽ thấy cùng một ring trên mọi phần tử. Không component nào ghi đè nó. Không variant nào xóa nó. Kết hợp với pattern disabled đồng nhất, người dùng luôn biết chính xác “không hoạt động” và “đang focus” trông như thế nào — bất kể họ đang tương tác với component nào.

Ngoại lệ duy nhất

Radio button dùng rounded-full.

Đó là ngoại lệ duy nhất trong toàn bộ thư viện component. Nó được ghi nhận trong component guidelines cùng với các quy tắc mà nó phá vỡ, vì ngay cả ngoại lệ cũng nên là quyết định, không phải sự cố.

Một radio button hình vuông sẽ gây nhầm lẫn cho người dùng. Hình tròn đã ăn sâu vào quy ước giao diện đến mức loại bỏ nó sẽ hy sinh khả năng sử dụng để theo đuổi sự thuần khiết thẩm mỹ. Chúng tôi quyết định đó là một đánh đổi tệ.

Bạn có thể hỏi: nếu khả năng sử dụng biện minh cho ngoại lệ ở radio, tại sao không cho avatar, tag, hay toggle switch? Chúng tôi đã thử. Avatar hình vuông với offset shadow trông giống profile card — chúng đọc như có chủ đích. Tag và toggle ít bị ràng buộc bởi quy ước hơn radio button. Affordance hình tròn đặc biệt mạnh cho radio; không gì khác vượt qua ngưỡng.

Mục tiêu không bao giờ là “brutalism vì brutalism.” Mà là loại bỏ các quyết định thị giác không xứng đáng với độ phức tạp mà chúng mang lại. Một radio button bo tròn xứng đáng với hình dạng của nó. Một card bo tròn thì không.

Cái giá thực sự

Chúng tôi sẽ không trung thực nếu nói cách tiếp cận này không có nhược điểm. Có đấy.

“Trông chưa hoàn thiện.” Đó là phản hồi thực từ user testing. Góc vuông và màu phẳng không truyền tải cảm giác tinh xảo như giao diện bo tròn, có shadow. Chúng tôi đã phải giải thích lý do nhiều hơn một lần.

Một số component cảm thấy cứng nhắc. Tooltip và popover, đặc biệt, trông nặng nề hơn với góc vuông. Blur shadow tồn tại vì lý do — chúng tạo cảm giác lơ lửng mà offset shadow không hoàn toàn tái tạo được.

Component bên thứ ba phá vỡ pattern. Date picker, rich text editor, thư viện biểu đồ — tất cả đều đi kèm border-radius và blur shadow được tích hợp sẵn. Chúng tôi override những gì có thể và chấp nhận một số không nhất quán ở rìa. Đây là căng thẳng đang diễn ra, không phải vấn đề đã giải quyết.

Sự sáng tạo của designer bị giới hạn. Nếu bạn là kiểu designer tìm thấy niềm vui trong việc tạo micro-interaction với gradient overlay và độ sâu tinh tế, hệ thống này sẽ cảm thấy hạn chế. Đó là mục đích — nhưng nó vẫn là chi phí thực cho sự thỏa mãn sáng tạo.

Nó gây chia rẽ. Một số kỹ sư yêu sự đơn giản. Số khác nghĩ nó giáo điều. Chúng tôi chưa hoàn toàn giải quyết căng thẳng này. Chúng tôi chỉ quyết định rằng lợi ích về tính nhất quán vượt trội hơn chi phí thẩm mỹ cho sản phẩm của mình.

Những gì chúng tôi chưa biết

Chúng tôi chưa thử nghiệm hệ thống này ở quy mô thực sự — hàng trăm component, giao diện dày đặc dữ liệu, tích hợp bên thứ ba cần trông tự nhiên. Table, chart và dashboard có thể cần các tín hiệu độ sâu mà offset shadow không thể cung cấp.

Chúng tôi thực sự không chắc liệu người dùng có phát triển cùng mức hiểu trực giác về các trạng thái shadow của chúng tôi (mặc định → hover → active) như họ có với hệ thống elevation truyền thống hay không. Chúng tôi chưa quan sát thấy sự suy giảm khả năng sử dụng, nhưng chúng tôi cũng chưa tiến hành A/B testing chính thức cho vấn đề này. Hiện tại nó đang hoạt động tốt. Nhưng “hiện tại” là một mẫu nhỏ.

Ràng buộc chính là quyết định

Mỗi ràng buộc trong hệ thống này là một quyết định được đưa ra một lần để không cần đưa ra lại. Không border-radius không phải là sở thích thẩm mỹ — đó là một ADR (Architecture Decision Record) cho toàn bộ thư viện component.

Nếu đội ngũ của bạn dành thời gian tranh luận về các thuộc tính thị giác không ảnh hưởng đến khả năng sử dụng, hãy cân nhắc loại bỏ hoàn toàn lựa chọn đó. Bạn có thể sẽ ít nhớ nó hơn bạn nghĩ.

Share