Design token với CSS custom property: vai trò, kế thừa và dự phòng
Dựng giao diện CSS có phạm vi, lần theo alias token được kế thừa và thử giới hạn của giá trị dự phòng trong var cùng kiểu đã đăng ký.

Design token đặt tên cho những quyết định mà nhiều thành phần nên cùng sử dụng. Thuộc tính tùy chỉnh của CSS, thường gọi là custom property, có thể mang các quyết định đó, nhưng vẫn tuân theo cascade, kế thừa và quy tắc xử lý giá trị. Một tên token hữu ích không biến giá trị thành thiết lập toàn cục luôn được tính lại, cũng không tự xác thực đầu vào.
Nếu bạn đã biết khai báo CSS cơ bản, bài này sẽ giúp bạn dựng một giao diện nhỏ và tìm nguyên nhân của hai lỗi khó hiểu: ghi đè màu trong bảng màu nhưng vai trò tham chiếu màu đó không đổi, và giá trị dự phòng không cứu được khoảng đệm sai kiểu. Bản xem trước màu ô liu và be là ví dụ đề xuất, không khẳng định mọi thành phần của portfolio này đang được triển khai theo cách đó.
Để thành phần yêu cầu một vai trò
Tên bảng màu như --olive-900 mô tả lựa chọn màu. Tên vai trò như --text-primary mô tả lý do thành phần cần giá trị ấy. Hôm nay chúng có thể cho cùng một màu nhưng đổi độc lập khi thiết kế lại. Thành phần cần màu chữ chính không phải biết màu đó là ô liu, xám than hay một màu khác đã được kiểm tra.
Dùng tài liệu nhỏ sau cho các ví dụ. Nút chỉ đổi bản xem trước này; nó không lưu lựa chọn, theo giao diện của hệ điều hành hay đổi toàn bộ ứng dụng. Liên kết ghi chú có đích fragment thực trong cùng tài liệu, nên cũng là một phần tử nhận focus bằng hành vi có sẵn của trình duyệt để bạn kiểm tra.
<section class="preview" id="token-preview" data-theme="light"
aria-labelledby="preview-title">
<h2 id="preview-title">Xem trước giao diện thẻ</h2>
<button id="theme-toggle" type="button" aria-pressed="false"
aria-controls="token-preview">Bản xem trước tối</button>
<article class="article-card">
<h3>Ghi chú về nhãn biểu mẫu</h3>
<p>Tên giúp người đọc gắn điều khiển với mục đích của nó.</p>
<a href="#token-notes">Đọc ghi chú của ví dụ</a>
</article>
<p class="probe">Thử khoảng đệm</p>
<p id="token-notes">Đây là nội dung mẫu để kiểm tra giao diện.</p>
</section>
:root {
--olive-900: #303326;
--olive-700: #696d45;
--beige-100: #f4f0e6;
--beige-200: #e7e3d7;
--surface-page: var(--beige-100);
--surface-card: var(--beige-200);
--text-primary: var(--olive-900);
--border-muted: var(--olive-700);
--focus-ring: var(--olive-700);
--space-card: 1.5rem;
}
.preview[data-theme="dark"] {
--surface-page: var(--olive-900);
--surface-card: var(--olive-900);
--text-primary: var(--beige-100);
--border-muted: var(--beige-200);
--focus-ring: var(--beige-100);
}
.preview {
box-sizing: border-box;
max-inline-size: 40rem;
padding: 1rem;
color: var(--text-primary);
background: var(--surface-page);
overflow-wrap: anywhere;
}
.article-card {
margin-block: 1rem;
padding: var(--space-card);
color: var(--text-primary);
background: var(--surface-card);
border: 1px solid var(--border-muted);
}
.preview a { color: inherit; }
.preview button {
max-inline-size: 100%;
padding: .6rem 1rem;
border: 1px solid currentColor;
font: inherit;
color: inherit;
background: var(--surface-card);
}
.preview :is(a, button):focus-visible {
outline: 3px solid var(--focus-ring);
outline-offset: 3px;
}
.probe {
padding: 2rem;
padding: var(--space-probe, 1rem);
}
Hướng dẫn custom property của MDN giải thích vì sao các khai báo này tham gia cơ chế kế thừa CSS thông thường. Phần tử gốc cung cấp bảng màu và các vai trò mặc định. Bản xem trước tối gán vai trò ngay tại ranh giới của nó. Thẻ dùng những vai trò ấy mà không cần selector nhận biết giao diện sáng hay tối.
Gắn script sau ở phía sau phần HTML. Nhãn nút giữ nguyên, còn trạng thái nhấn cho biết bản xem trước tối có đang được chọn không. Hành vi kích hoạt nút có sẵn hỗ trợ cả thiết bị trỏ lẫn bàn phím; không cần tự viết bộ xử lý phím.
const preview = document.querySelector('#token-preview');
const toggle = document.querySelector('#theme-toggle');
toggle.addEventListener('click', () => {
const dark = toggle.getAttribute('aria-pressed') !== 'true';
toggle.setAttribute('aria-pressed', String(dark));
preview.dataset.theme = dark ? 'dark' : 'light';
});
Khi định nghĩa giao diện, hãy giữ các màu liên quan thành một nhóm. Đổi nền có thể làm chữ hoặc đường viền focus không đổi trở nên khó thấy. Ví dụ này đổi đồng thời vai trò chữ, bề mặt, viền và focus, nhưng các tên đó không tự chứng minh độ tương phản đủ dùng. Kiểm tra những tổ hợp được hiển thị, cả khi có focus, và thử bảng màu mới trước khi chấp nhận nó.
Lớp vai trò có giá trị khi diễn tả một quan hệ thực sự. Thành phần nhỏ không cần token riêng cho từng pixel trang trí. Bắt đầu từ quyết định được lặp lại, rồi tách thêm khi hai nơi sử dụng thực sự cần thay đổi độc lập.
Alias được kế thừa đã được giải quyết tham chiếu
Ở đây alias là custom property có giá trị tham chiếu một custom property khác. Có khác biệt tinh tế giữa ghi đè một vai trò và ghi đè giá trị gốc mà vai trò đó tham chiếu. Trong bản xem trước sáng, đặt --beige-200: pink trực tiếp lên phần tử preview. Thẻ không chuyển sang hồng. Giá trị --surface-card được kế thừa đã được tính tại phần tử gốc, nơi màu be vẫn giữ giá trị ban đầu.
Đặc tả custom property nói rằng tham chiếu biến được giải quyết khi tính giá trị, trước khi kế thừa. Phần tử con nhận alias đã được giải quyết, không nhận chỉ dẫn tìm lại mọi giá trị phụ thuộc theo bảng màu của chính nó. Vì vậy selector tối phía trên gán lại các vai trò ngữ nghĩa ngay tại ranh giới giao diện cần thay đổi.
Bây giờ đặt thêm --surface-card: var(--beige-200) lên bản xem trước sáng. Alias được giải quyết tại đó, thấy màu hồng cục bộ rồi truyền tới thẻ qua kế thừa. Sau đó xóa cả hai ghi đè. Một lựa chọn khác là đổi màu gốc ngay trên phần tử gốc; khi ấy alias được tính lại tại nơi nó được khai báo. Đây là các thao tác khác nhau với phạm vi tác động khác nhau.
Khai báo cục bộ trong thẻ cũng có thể chặn vai trò được ghi đè từ tổ tiên. Nếu thẻ tự khai báo --text-primary, giao diện cấp trang không thể thay giá trị ấy chỉ nhờ kế thừa. Hãy xem khai báo nào đang cung cấp giá trị trước khi thêm quy tắc cạnh tranh. Phần tử con kế thừa từ tổ tiên, không kế thừa từ một bản xem trước anh em nằm cạnh nó.
Giá trị dự phòng không xác thực padding
Phần tử probe có hai khai báo khoảng đệm. Khai báo đầu là 2rem; khai báo sau dùng --space-probe với giá trị dự phòng 1rem. Trong tài liệu chưa chỉnh sửa, không nơi nào định nghĩa custom property đó, nên khai báo thứ hai dùng giá trị dự phòng.
Chạy thí nghiệm console sau khi cỡ chữ tính toán của phần tử gốc là 16px. Nó chỉ đổi custom property trên probe rồi khôi phục style nội tuyến ban đầu. Ví dụ giả định không tổ tiên nào khai báo thuộc tính riêng cho probe này; nếu có, xóa khai báo cục bộ sẽ làm lộ giá trị được kế thừa.
{
const probe = document.querySelector('.probe');
const originalStyle = probe.getAttribute('style');
const rows = [];
try {
for (const [name, value] of [
['missing', null], ['length', '24px'],
['wrong type', 'red'], ['initial', 'initial']
]) {
if (value === null) probe.style.removeProperty('--space-probe');
else probe.style.setProperty('--space-probe', value);
rows.push({name, padding: getComputedStyle(probe).paddingTop});
}
console.table(rows);
} finally {
if (originalStyle === null) probe.removeAttribute('style');
else probe.setAttribute('style', originalStyle);
}
}
Các giá trị khoảng đệm dự kiến lần lượt là 16px, 24px, 0px và 16px. Giá trị 0 gây ngạc nhiên thuộc trường hợp red. Custom property chưa đăng ký kiểu có thể chứa token đó, nên bước thay thế thành công. Tuy nhiên, giá trị padding sau thay thế không hợp lệ: màu không phải độ dài khoảng đệm. Giá trị dự phòng trong var không được chọn vì bản thân custom property đã có giá trị dùng để thay thế.
Trình duyệt cũng không quay lại khai báo 2rem đứng trước. Quy tắc biến không hợp lệ phân biệt lỗi cú pháp bị loại sớm với giá trị chỉ trở nên sai sau khi thay thế. Ở giai đoạn sau này, padding là thuộc tính không kế thừa nên nhận giá trị khởi tạo 0. Thuộc tính có kế thừa như color sẽ nhận giá trị được kế thừa trong trường hợp lỗi tương đương.
Với custom property chưa đăng ký này, từ khóa chung của CSS initial khôi phục giá trị khởi tạo được bảo đảm không hợp lệ của nó, cho phép nhánh dự phòng trong var chạy. Khai báo một giá trị token rỗng khác với giá trị không hợp lệ đặc biệt ấy; đừng coi mọi chuỗi rỗng trong công cụ phát triển là bằng chứng thuộc tính đang thiếu. Chữ hoa cũng có ý nghĩa: --Space-probe và --space-probe là hai tên riêng biệt.
Hình tách hai quyết định: trước tiên việc thay thế có giá trị custom property dùng được hay không, sau đó thuộc tính nhận giá trị có chấp nhận các token đã thay hay không. Phân biệt hai bước đáng tin hơn việc nhớ rằng biến có dự phòng rồi cho rằng mọi lỗi đều đi tới nhánh đó.
Đăng ký thuộc tính tạo ra hợp đồng khác
Sau khi ghi lại kết quả bốn trạng thái, thêm CSS sau để làm thí nghiệm riêng. Nó đổi phần tử probe sang dùng một thuộc tính đã đăng ký. Khi quy tắc này còn áp dụng, đổi --space-probe không còn điều khiển khoảng đệm của probe. Xóa quy tắc mới thêm trước khi chạy lại bảng kết quả trước đó.
@property --space-checked {
syntax: "<length>";
inherits: false;
initial-value: 24px;
}
.probe { padding: var(--space-checked, 16px); }
Tài liệu quy tắc property mô tả các descriptor cho cú pháp, kế thừa và giá trị khởi tạo. Khi không có giá trị cục bộ ở đây, probe nhận 24px, không phải 16px bên trong var. Việc đăng ký đã cung cấp một giá trị khởi tạo hợp lệ. Vì kế thừa bị tắt, đặt giá trị khác của thuộc tính đã đăng ký chỉ trên phần tử cha sẽ không đổi khoảng đệm của probe.
Đặc tả đăng ký yêu cầu giá trị khởi tạo độc lập về tính toán với cú pháp này. Giá trị cố định 24px đáp ứng yêu cầu; giá trị khởi tạo phụ thuộc vào cỡ chữ của phần tử thì không. Vai trò khoảng cách thông thường chưa đăng ký vẫn có thể dùng rem, như thẻ trong ví dụ. Đừng đổi tất cả thành độ dài cố định chỉ để tiện đăng ký.
Ví dụ đăng ký giả định trình duyệt có triển khai @property. Nếu quy tắc không được hỗ trợ và bị bỏ qua, thuộc tính vẫn giữ hành vi chưa đăng ký; hãy kiểm tra các trình duyệt mà giao diện của bạn hỗ trợ.
Hợp đồng kiểu cũng có giới hạn. Độ dài âm phù hợp với cú pháp độ dài đã đăng ký nhưng không hợp lệ cho padding. Thử đặt --space-checked: -4px trên probe: thuộc tính nhận giá trị vẫn có thể trở nên không hợp lệ và dùng 0. Xác thực thiết lập giao diện từ bên ngoài phải xét ràng buộc thực sự của nơi nhận, không chỉ tên token hoặc kiểu rộng của nó.
Rà soát thay đổi token tại nơi sử dụng
Dùng Tab và Space trên nút xem trước, rồi Tab tới liên kết của nó. Kiểm tra dấu focus vẫn rõ sau khi đổi giao diện và nhãn không đổi nghĩa. Phóng to chữ, xác nhận thẻ cao lên thay vì cắt nội dung dài hơn. Chỉ nhìn bảng màu không thể phát hiện những tương tác đó.
Không tràn chỉ là một phần của việc rà soát. Với khung nhìn 320px và cỡ chữ gốc 32px, thẻ trong ví dụ có khoảng đệm 48px ở mỗi bên. Cột chữ còn lại hẹp tới mức các từ thông thường cũng có thể bị ngắt. Đặt --space-card: .75rem trên bản xem trước rồi so sánh độ dài dòng. Ghi đè vai trò cục bộ này giảm khoảng đệm xuống 24px mà không thu nhỏ chữ. Kiểm tra cả cỡ chữ thông thường lẫn phóng to trước khi chọn khoảng cách dễ sử dụng.
Để luyện tập, đổi tên --text-primary trong toàn bộ stylesheet, nhưng cố ý giữ nguyên tham chiếu màu chữ của thẻ. Lần theo màu tính toán nhận được rồi sửa nơi đó. Bài tập cho thấy vì sao đổi tên cần tìm các tham chiếu và kiểm tra kết quả hiển thị. Màu được kế thừa hoặc giá trị dự phòng trông hợp lý có thể che một lần chuyển đổi chưa hoàn tất. Giữ bộ tên token đủ nhỏ để người rà soát giải thích được mỗi thay đổi dự định ảnh hưởng tới thành phần nào.