Lợi ích của việc sử dụng Thuộc tính tuỳ chỉnh trong hệ thống thiết kế và thư viện thành phần.
Tôi là Dave, một Nhà phát triển cấp cao về giao diện người dùng tại Nordhealth. Tôi làm việc về thiết kế và phát triển hệ thống thiết kế Nord của chúng tôi, bao gồm cả việc xây dựng các Thành phần web cho thư viện thành phần của chúng tôi. Tôi muốn chia sẻ cách chúng tôi giải quyết các vấn đề về việc tạo kiểu cho Thành phần web bằng cách sử dụng Thuộc tính tuỳ chỉnh CSS và một số lợi ích khác khi sử dụng Thuộc tính tuỳ chỉnh trong hệ thống thiết kế và thư viện thành phần.
Cách chúng tôi tạo Thành phần web
Để tạo Web Components (Thành phần web), chúng ta sẽ dùng Lit, một thư viện cung cấp nhiều mã khởi động như trạng thái, kiểu có phạm vi, mẫu và nhiều mã khác. Lit không chỉ có dung lượng nhẹ mà còn được xây dựng trên các API JavaScript gốc, nghĩa là chúng ta có thể phân phối một gói mã tinh gọn tận dụng các tính năng mà trình duyệt đã có.
import {html, css, LitElement} from 'lit';
export class SimpleGreeting extends LitElement {
static styles = css`:host { color: blue; font-family: sans-serif; }`;
static properties = {
name: {type: String},
};
constructor() {
super();
this.name = 'there';
}
render() {
return html`Hey
${this.name}, welcome to Web Components!`;
}
}
customElements.define('simple-greeting', SimpleGreeting);
Nhưng điều hấp dẫn nhất về Web Components là chúng hoạt động với hầu hết mọi khung JavaScript hiện có, hoặc thậm chí không cần khung nào. Sau khi gói JavaScript chính được tham chiếu trong trang, việc sử dụng một Thành phần web cũng giống như việc sử dụng một phần tử HTML gốc. Dấu hiệu duy nhất cho thấy đây không phải là một phần tử HTML gốc là dấu gạch ngang nhất quán trong các thẻ. Đây là tiêu chuẩn để báo hiệu cho trình duyệt rằng đây là một Thành phần web.
Đóng gói kiểu Shadow DOM
Tương tự như các phần tử HTML gốc có Shadow DOM, Thành phần web cũng vậy. Shadow DOM là một cây nút ẩn trong một phần tử. Cách tốt nhất để hình dung điều này là mở trình kiểm tra web và bật lựa chọn "Show Shadow DOM tree" (Hiện cây DOM tối). Sau khi thực hiện việc này, hãy thử xem xét một phần tử đầu vào gốc trong trình kiểm tra. Giờ đây, bạn sẽ có lựa chọn mở phần tử đầu vào đó và xem tất cả các phần tử trong phần tử đó. Bạn thậm chí có thể thử điều này với một trong các Thành phần web của chúng tôi – hãy thử kiểm tra thành phần đầu vào tuỳ chỉnh của chúng tôi để xem DOM tối của thành phần đó.

Một trong những ưu điểm (hoặc nhược điểm, tuỳ thuộc vào quan điểm của bạn) của Shadow DOM là khả năng đóng gói kiểu. Nếu bạn viết CSS trong Thành phần web, thì các kiểu đó không thể rò rỉ và ảnh hưởng đến trang chính hoặc các phần tử khác; chúng hoàn toàn nằm trong thành phần. Ngoài ra, CSS được viết cho trang chính hoặc một Thành phần web mẹ không thể rò rỉ vào Thành phần web của bạn.
Việc đóng gói các kiểu này là một lợi ích trong thư viện thành phần của chúng ta. Điều này giúp chúng ta đảm bảo hơn rằng khi người dùng sử dụng một trong các thành phần của chúng ta, thành phần đó sẽ trông như chúng ta dự định, bất kể kiểu nào được áp dụng cho trang mẹ. Để đảm bảo hơn nữa, chúng ta sẽ thêm all: unset;
vào gốc hoặc "máy chủ" của tất cả các Thành phần web.
:host {
all: unset;
display: block;
box-sizing: border-box;
text-align: start;
/* ... */
}
Tuy nhiên, nếu người dùng Thành phần web của bạn có lý do chính đáng để thay đổi một số kiểu thì sao? Có thể có một dòng văn bản cần tăng độ tương phản do ngữ cảnh của dòng đó, hoặc một đường viền cần dày hơn? Nếu không có kiểu nào có thể đưa vào thành phần của bạn, thì làm cách nào để bạn có thể sử dụng các lựa chọn tạo kiểu đó?
Đó là lúc Thuộc tính tuỳ chỉnh của CSS xuất hiện.
Thuộc tính tuỳ chỉnh của CSS
Thuộc tính tuỳ chỉnh có tên gọi rất phù hợp – đây là các thuộc tính CSS mà bạn có thể tự đặt tên hoàn toàn và áp dụng bất kỳ giá trị nào cần thiết. Yêu cầu duy nhất là bạn phải thêm tiền tố là hai dấu gạch ngang. Sau khi bạn khai báo thuộc tính tuỳ chỉnh, bạn có thể dùng giá trị này trong CSS bằng hàm var()
.
:root {
--n-color-accent: rgb(53, 89, 199);
/* ... */
}
.n-color-accent-text {
color: var(--n-color-accent);
}
Khi nói đến tính kế thừa, tất cả các Thuộc tính tuỳ chỉnh đều được kế thừa, tuân theo hành vi thông thường của các thuộc tính và giá trị CSS thông thường. Bạn có thể dùng mọi thuộc tính tuỳ chỉnh được áp dụng cho một phần tử mẹ hoặc chính phần tử đó làm giá trị cho các thuộc tính khác. Chúng tôi sử dụng nhiều Thuộc tính tuỳ chỉnh cho mã thiết kế bằng cách áp dụng các thuộc tính này cho phần tử gốc thông qua Khung CSS. Điều này có nghĩa là tất cả các phần tử trên trang đều có thể sử dụng các giá trị mã này, cho dù đó là Thành phần web, lớp trợ giúp CSS hay nhà phát triển muốn lấy một giá trị từ danh sách mã của chúng tôi.
Khả năng kế thừa Thuộc tính tuỳ chỉnh, thông qua việc sử dụng hàm var()
, là cách chúng ta xuyên qua Shadow DOM của Thành phần web và cho phép nhà phát triển kiểm soát chi tiết hơn khi tạo kiểu cho các thành phần.
Thuộc tính tuỳ chỉnh trong thành phần web Nord
Bất cứ khi nào phát triển một thành phần cho hệ thống thiết kế của mình, chúng tôi đều áp dụng một phương pháp chu đáo cho CSS của thành phần đó. Chúng tôi muốn hướng đến mã nguồn tinh gọn nhưng rất dễ bảo trì. Các mã thiết kế mà chúng tôi có được xác định là Thuộc tính tuỳ chỉnh trong Khung CSS chính trên phần tử gốc.
:root {
--n-space-m: 16px;
--n-space-l: 24px;
/* ... */
--n-color-background: rgb(255, 255, 255);
--n-color-border: rgb(216, 222, 228);
/* ... */
}
Sau đó, các giá trị mã thông báo này sẽ được tham chiếu trong các thành phần của chúng tôi. Trong một số trường hợp, chúng ta sẽ áp dụng giá trị trực tiếp cho thuộc tính CSS, nhưng đối với những trường hợp khác, chúng ta sẽ thực sự xác định một Thuộc tính tuỳ chỉnh theo bối cảnh mới và áp dụng giá trị cho thuộc tính đó.
:host {
--n-tab-group-padding: 0;
--n-tab-list-background: var(--n-color-background);
--n-tab-list-border: inset 0 -1px 0 0 var(--n-color-border);
/* ... */
}
.n-tab-group-list {
box-shadow: var(--n-tab-list-border);
background-color: var(--n-tab-list-background);
gap: var(--n-space-s);
/* ... */
}
Chúng ta cũng sẽ trừu tượng hoá một số giá trị dành riêng cho thành phần nhưng không có trong mã thông báo và biến chúng thành một Thuộc tính tuỳ chỉnh theo bối cảnh. Các Thuộc tính tuỳ chỉnh theo ngữ cảnh của thành phần mang lại cho chúng ta 2 lợi ích chính. Trước tiên, điều này có nghĩa là chúng ta có thể sử dụng CSS "đơn giản" hơn vì giá trị đó có thể được áp dụng cho nhiều thuộc tính bên trong thành phần.
.n-tab-group-list::before {
/* ... */
padding-inline-start: var(--n-tab-group-padding);
}
.n-tab-group-list::after {
/* ... */
padding-inline-end: var(--n-tab-group-padding);
}
Thứ hai, nó giúp các thay đổi về trạng thái và biến thể của thành phần trở nên thực sự rõ ràng – bạn chỉ cần thay đổi thuộc tính tuỳ chỉnh để cập nhật tất cả các thuộc tính đó khi, chẳng hạn như, bạn đang tạo kiểu cho trạng thái di chuột hoặc trạng thái hoạt động hoặc trong trường hợp này là một biến thể.
:host([padding="l"]) {
--n-tab-group-padding: var(--n-sp
ace-l);
}
Nhưng lợi ích mạnh mẽ nhất là khi xác định các Thuộc tính tuỳ chỉnh theo bối cảnh này trên một thành phần, chúng ta sẽ tạo một loại API CSS tuỳ chỉnh cho từng thành phần của mình. Người dùng thành phần đó có thể khai thác API này.
<nord-tab-group label="T>itl<e"
>!<-- ... --
/nord>-t<ab-gr>oup
style
nord-tab-group {
--n-tab-group-padding: var(--n-space<-xl);
>
}
/style
Ví dụ trước đó cho thấy một trong các Thành phần web của chúng tôi có Thuộc tính tuỳ chỉnh theo bối cảnh được thay đổi thông qua bộ chọn. Kết quả của toàn bộ phương pháp này là một thành phần mang lại cho người dùng đủ sự linh hoạt về kiểu dáng, đồng thời vẫn kiểm soát được hầu hết các kiểu dáng thực tế. Ngoài ra, chúng ta (nhà phát triển thành phần) có thể chặn những kiểu do người dùng áp dụng. Nếu muốn điều chỉnh hoặc mở rộng một trong những thuộc tính đó, chúng ta có thể làm mà không cần người dùng thay đổi bất kỳ mã nào của họ.
Chúng tôi nhận thấy phương pháp này cực kỳ hiệu quả, không chỉ đối với chúng tôi với tư cách là nhà sáng tạo các thành phần trong hệ thống thiết kế, mà còn đối với nhóm phát triển khi họ sử dụng các thành phần này trong sản phẩm của chúng tôi.
Tìm hiểu thêm về Thuộc tính tuỳ chỉnh
Tại thời điểm viết bài, chúng tôi chưa thực sự tiết lộ các Thuộc tính tuỳ chỉnh theo bối cảnh này trong tài liệu của mình; tuy nhiên, chúng tôi dự định sẽ làm như vậy để nhóm phát triển rộng hơn của chúng tôi có thể hiểu và tận dụng các thuộc tính này. Các thành phần của chúng tôi được đóng gói trên npm bằng tệp kê khai. Tệp này chứa mọi thông tin cần biết về các thành phần đó. Sau đó, chúng tôi sử dụng tệp kê khai làm dữ liệu khi triển khai trang web tài liệu của mình. Việc này được thực hiện bằng cách sử dụng Eleventy và tính năng Dữ liệu toàn cầu của Eleventy. Chúng tôi dự định sẽ đưa những Thuộc tính tuỳ chỉnh theo bối cảnh này vào tệp dữ liệu kê khai.
Một khía cạnh khác mà chúng tôi muốn cải thiện là cách các Thuộc tính tuỳ chỉnh theo bối cảnh này kế thừa giá trị. Ví dụ: hiện tại, nếu muốn điều chỉnh màu của 2 thành phần đường phân chia, bạn cần nhắm đến cả 2 thành phần đó một cách cụ thể bằng bộ chọn hoặc áp dụng thuộc tính tuỳ chỉnh trực tiếp trên phần tử bằng thuộc tính kiểu. Điều này có vẻ ổn, nhưng sẽ hữu ích hơn nếu nhà phát triển có thể xác định những kiểu đó trên một phần tử chứa hoặc thậm chí ở cấp gốc.
<nord-divider></nord-divider>
<section>
<nord-divider></nord-divider>
<!-- ... -->
</section>
<style>
nord-divider {
--n-divider-color: var(--n-color-status-danger);
}
section {
padding: var(--n-space-s);
background: var(--n-color-surface-raised);
}
section nord-divider {
--n-divider-color: var(--n-color-status-success);
}
</style>
Lý do bạn phải đặt giá trị Thuộc tính tuỳ chỉnh trực tiếp trên thành phần là vì chúng ta đang xác định các giá trị này trên cùng một phần tử thông qua bộ chọn máy chủ lưu trữ thành phần. Các mã thiết kế chung mà chúng ta sử dụng trực tiếp trong thành phần sẽ truyền thẳng qua, không bị ảnh hưởng bởi vấn đề này và thậm chí có thể bị chặn trên các phần tử mẹ. Làm cách nào để tận dụng được cả hai?
Thuộc tính tuỳ chỉnh riêng tư và công khai
Thuộc tính tuỳ chỉnh riêng tư là một thuộc tính do Lea Verou tạo ra. Đây là Thuộc tính tuỳ chỉnh "riêng tư" theo ngữ cảnh trên chính thành phần đó nhưng được đặt thành Thuộc tính tuỳ chỉnh "công khai" với một giá trị dự phòng.
:host {
--_n-divider-color: var(--n-divider-color, var(--n-color-border));
--_n-divider-size: var(--n-divider-size, 1px);
}
.n-divider {
border-block-start: solid var(--_n-divider-size) var(--_n-divider-color);
/* ... */
}
Việc xác định Thuộc tính tuỳ chỉnh theo bối cảnh theo cách này có nghĩa là chúng ta vẫn có thể làm tất cả những việc đã làm trước đây, chẳng hạn như kế thừa các giá trị mã thông báo chung và sử dụng lại các giá trị trong mã thành phần; nhưng thành phần cũng sẽ kế thừa một cách linh hoạt các định nghĩa mới của thuộc tính đó trên chính nó hoặc bất kỳ phần tử mẹ nào.
<nord-divider></nord-divider>
<section>
<nord-divider></nord-divider>
<!-- ... -->
</section>
<style>
nord-divider {
--n-divider-color: var(--n-color-status-danger);
}
section {
padding: var(--n-space-s);
background: var(--n-color-surface-raised);
--n-divider-color: var(--n-color-status-success);
}
</style>
Mặc dù có thể cho rằng phương thức này không thực sự "riêng tư", nhưng chúng tôi vẫn cho rằng đây là một giải pháp khá hiệu quả cho vấn đề mà chúng tôi lo ngại. Khi có cơ hội, chúng tôi sẽ giải quyết vấn đề này trong các thành phần của mình để nhóm phát triển có nhiều quyền kiểm soát hơn đối với việc sử dụng thành phần mà vẫn được hưởng lợi từ các nguyên tắc mà chúng tôi đã đặt ra.
Tôi hy vọng bạn thấy thông tin chi tiết này về cách chúng tôi sử dụng Thành phần web với Thuộc tính tuỳ chỉnh CSS là hữu ích. Hãy cho chúng tôi biết ý kiến của bạn. Nếu quyết định sử dụng bất kỳ phương pháp nào trong số này cho công việc của mình, bạn có thể tìm thấy tôi trên Twitter @DavidDarnes. Bạn cũng có thể tìm thấy Nordhealth @NordhealthHQ trên Twitter, cũng như những thành viên khác trong nhóm của tôi. Họ đã nỗ lực hết mình để xây dựng hệ thống thiết kế này và triển khai các tính năng được đề cập trong bài viết này: @Viljamis, @WickyNilliams và @eric_habich.
Hình ảnh chính của Dan Cristian Pădureț