Cách Nordhealth sử dụng các thuộc tính tuỳ chỉnh trong Thành phần web

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.

David Darnes
David Darnes

Tôi là Dave, Nhà phát triển front-end cấp cao tại Nordhealth. Tôi phụ trách thiết kế và phát triển hệ thống thiết kế Nord, bao gồm cả việc xây dựng 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 đề liên quan đến 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, cũng như một số lợi ích khác 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.

Cách chúng tôi xây dựng Thành phần web

Để xây dựng Thành phần web, chúng tôi sử dụng Lit, một thư viện cung cấp nhiều mã nguyên mẫu như trạng thái, kiểu trong phạm vi, mẫu và nhiều mã khác. Lit không chỉ gọn nhẹ mà còn được xây dựng trên các API JavaScript gốc, nghĩa là chúng tôi có thể cung cấp 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);
Một Web Component được viết bằng Lit.

Tuy nhiên, điều hấp dẫn nhất về Thành phần web 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ó khung nào cả. Sau khi gói JavaScript chính được tham chiếu trong trang, việc sử dụng Thành phần web sẽ rất giống với việc sử dụng 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 nối nhất quán trong các thẻ. Đây là một tiêu chuẩn để báo cho trình duyệt biế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 hay nhất để hình dung điều này là mở trình kiểm tra web và bật tuỳ chọn "Show Shadow DOM tree" (Hiện cây DOM tối). Sau khi thực hiện xong, hãy thử xem một phần tử đầu vào gốc trong trình kiểm tra. Giờ đây, bạn có thể mở phần tử đầu vào đó và xem tất cả các phần tử bên trong. Bạn thậm chí có thể thử cách 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 Shadow DOM của thành phần đó.

DOM tối được kiểm tra trong Công cụ cho nhà phát triển.
Ví dụ về Shadow DOM trong một phần tử nhập văn bản thông thường và trong Thành phần web nhập Nord.

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à đó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ỉ ra ngoài 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 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 tôi. Điều này giúp chúng tôi đảm bảo rằng khi người dùng sử dụng một trong các thành phần của chúng tôi, thành phần đó sẽ hiển thị như chúng tôi dự định, bất kể kiểu được áp dụng cho trang mẹ. Và để đảm bảo hơn nữa, chúng ta thêm all: unset; vào gốc hoặc "máy chủ" của tất cả Thành phần web.


:host {
  all: unset;
  display: block;
  box-sizing: border-box;
  text-align: start;
  /* ... */
}
Một số mã nguyên mẫu của thành phần được áp dụng cho gốc bóng hoặc bộ chọn máy chủ.

Tuy nhiên, điều gì sẽ xảy ra 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 nhất định? Có thể có một dòng văn bản cần độ tương phản cao hơn do ngữ cảnh của dòng đó, hoặc đường viền cần dày hơn? Nếu không có kiểu nào có thể áp dụng cho thành phần của bạn, thì làm cách nào để bạn có thể sử dụng các tuỳ chọn tạo kiểu đó?

Đó là lúc các thuộc tính tuỳ chỉnh CSS phát huy tác dụng.

Thuộc tính tuỳ chỉnh CSS

Thuộc tính tuỳ chỉnh được đặt tên rất phù hợp – đó là các thuộc tính CSS mà bạn có thể tự đặt tê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ố gồm hai dấu gạch nối. Sau khi bạn khai báo thuộc tính tuỳ chỉnh, giá trị có thể được sử dụng trong CSS bằng hàm var().


:root {
  --n-color-accent: rgb(53, 89, 199);
  /* ... */
}

.n-color-accent-text {
  color: var(--n-color-accent);
}
Ví dụ về mã thông báo thiết kế dưới dạng thuộc tính tuỳ chỉnh trong Khung CSS của chúng tôi và mã thông báo này được dùng trên một lớp trợ giúp.

Khi nói đến tính kế thừa, tất 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 bất kỳ thuộc tính tuỳ chỉnh nào đượ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ã thông báo thiết kế bằng cách áp dụng các thuộc tính đó cho phần tử gốc bằng Khung CSS. Điều này có nghĩa là tất cả phần tử trên trang đều có thể sử dụng các giá trị mã thông báo 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ã thông báo của chúng tôi.

Khả năng kế thừa các thuộc tính tuỳ chỉnh này, cùng với việc sử dụng hàm var(), là cách chúng ta thâm nhập vào Shadow DOM của Thành phần web và cho phép nhà phát triển có quyề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 một 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ế, chúng tôi đều áp dụng phương pháp tiếp cận chu đáo cho CSS của thành phần đó. Chúng tôi muốn hướng đến mã tinh gọn nhưng rất dễ bảo trì. Mã thông báo 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);
  /* ... */
}
Các thuộc tính tuỳ chỉnh CSS được xác định trên bộ chọn gốc.

Sau đó, các giá trị mã thông báo này đượ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 tôi sẽ áp dụng giá trị trực tiếp trên thuộc tính CSS, nhưng đối với những trường hợp khác, chúng tôi 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);
  /* ... */
}
Các thuộc tính tuỳ chỉnh được xác định trên gốc bóng của thành phần, sau đó được dùng trong kiểu thành phần. Các thuộc tính tuỳ chỉnh trong danh sách mã thông báo thiết kế cũng đang được sử dụng.

Chúng ta cũng sẽ tóm tắt 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 các giá trị đó 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 hai lợi ích chính. Thứ nhất, điều này có nghĩa là chúng ta có thể viết CSS ngắn gọ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);
}
Thuộc tính tuỳ chỉnh theo bối cảnh của khoảng đệm nhóm thẻ được dùng ở nhiều nơi trong mã thành phần.

Thứ hai, thuộc tính này giúp trạng thái thành phần và các thay đổi về biến thể trở nên rõ ràng – 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 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-space-l);
}
Một biến thể của thành phần thẻ, trong đó khoảng đệm được thay đổi bằng một bản cập nhật thuộc tính tuỳ chỉnh duy nhất thay vì nhiều bản cập nhật.

Tuy nhiên, 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, người dùng thành phần đó có thể khai thác API này.


<nord-tab-group label="Title">
  <!-- ... -->
</nord-tab-group>

<style>
  nord-tab-group {
    --n-tab-group-padding: var(--n-space-xl);
  }
</style>
Sử dụng thành phần nhóm thẻ trên trang và cập nhật thuộc tính tuỳ chỉnh khoảng đệm thành kích thước lớn hơn.

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 bằng bộ chọn. Kết quả của toàn bộ phương pháp này là một thành phần cung cấp đủ kiểu linh hoạt cho người dùng trong khi vẫn kiểm soát được hầu hết các kiểu thực tế. Ngoài ra, chúng tôi với tư cách là nhà phát triển thành phần có thể chặn các 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 các thuộc tính đó, chúng ta có thể thực hiện mà không cần người dùng thay đổi bất kỳ mã nào.

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à người tạo ra các thành phần 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 sâu hơn về thuộc tính tuỳ chỉnh

Tại thời điểm viết bài, chúng tôi không 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 làm như vậy để nhóm phát triển rộng hơn 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 một tệp kê khai, 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 trang web tài liệu được triển khai bằng Eleventy và tính năng Dữ liệu toàn cầu của công cụ này. Chúng tôi dự định đưa các thuộc tính tuỳ chỉnh theo bối cảnh này vào tệp dữ liệu kê khai này.

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ụ: nếu muốn điều chỉnh màu của hai thành phần đường phân chia, bạn cần nhắm đến cả hai thành phần đó 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ử có 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 các 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>
Hai phiên bản của thành phần đường phân chia cần hai cách xử lý màu khác nhau. Một đường phân chia được lồng bên trong một phần mà chúng ta có thể sử dụng cho một bộ chọn cụ thể hơn, nhưng chúng ta phải nhắm mục tiêu cụ thể đến đường phân chia.

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ị đó trên cùng một phần tử bằng bộ chọn máy chủ thành phần. Mã thông báo thiết kế chung mà chúng ta sử dụng trực tiếp trong thành phần sẽ được truyền thẳng, 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 thế nào để chúng ta có thể tận dụng 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 tính năng do Lea Verou kết hợp, là một thuộc tính tuỳ chỉnh "riêng tư" theo bối 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" có tính năng 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);
  /* ... */
}
CSS của Thành phần web đường phân chia có các thuộc tính tuỳ chỉnh theo bối cảnh được điều chỉnh để CSS nội bộ dựa vào một thuộc tính tuỳ chỉnh riêng tư, thuộc tính này đã được đặt thành một thuộc tính tuỳ chỉnh công khai có giá trị dự phòng.

Việc xác định các thuộc tính tuỳ chỉnh theo ngữ 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 toàn bộ mã thành phần; nhưng thành phần này 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>
Hai đường phân chia, nhưng lần này, bạn có thể đổi màu đường phân chia bằng cách thêm thuộc tính tuỳ chỉnh theo bối cảnh của đường phân chia vào bộ chọn mục. Đường phân chia sẽ kế thừa màu này, tạo ra một đoạn mã gọn gàng và linh hoạt hơn.

Mặc dù có thể có ý kiến 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á tinh tế 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, trong khi vẫn hưởng lợi từ các biện pháp bảo vệ mà chúng tôi áp dụng.

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 các thuộc tính tuỳ chỉnh CSS hữu ích. Hãy cho chúng tôi biết suy nghĩ của bạn và nếu bạn quyết định sử dụng bất kỳ phương thức 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 còn lại trong nhóm của tôi, những người đã làm việc chăm chỉ để kết hợp hệ thống thiết kế này và triển khai các tính năng được đề cập trong tài liệu này: @Viljamis, @WickyNilliams và @eric_habich.