Tạo kiểu cho thành phần web

DOM tối là ranh giới kiểu: quy tắc trang không áp dụng cho DOM tối và quy tắc thành phần không áp dụng cho trang, ngoại trừ các thuộc tính kế thừa (bao gồm cả thuộc tính tuỳ chỉnh). Hãy chủ động vượt qua ranh giới đó thông qua các kênh dưới đây.

:host, :host(), :host-context()

/* The component's own host element, styled from inside its shadow tree. */
:host { display: block; }

/* Only when the host matches a selector (e.g. a reflected state attribute). */
:host([disabled]) { opacity: 0.5; pointer-events: none; }

/* Based on an ancestor in the Light DOM; useful for theming. */
:host-context([theme="dark"]) { background: #111; color: #eee; }

Tránh :host-context(). Bộ chọn này vươn ra bên ngoài ranh giới để kiểm tra các phần tử cấp trên, nhưng Firefox và Safari đã từ chối cung cấp bộ chọn này và CSSWG đang có xu hướng không phát triển thêm bộ chọn này. Hãy coi đây là một ngõ cụt chỉ dành cho Chromium, chứ không phải là một lựa chọn hướng tới tương lai. Thay vào đó, hãy tạo giao diện thông qua một thuộc tính tuỳ chỉnh CSS kế thừa (bên dưới): người dùng đặt thuộc tính trên một thành phần cấp trên và thành phần này đọc thuộc tính đó trên ranh giới, hoạt động ở mọi nơi và giữ cho cả hai bên tách biệt.

Các thuộc tính kế thừa và thuộc tính tuỳ chỉnh vượt qua ranh giới

Các thuộc tính CSS kế thừa (color, font-family, line-height và các thuộc tính tuỳ chỉnh CSS) có truyền qua ranh giới bóng. Điều này giúp các thuộc tính tuỳ chỉnh trở thành API giao diện chính: hiển thị các thuộc tính đó bằng các phương án dự phòng hợp lý.

:host {
  /* Consumers override --card-bg from outside; the fallback keeps it usable. */
  background: var(--card-bg, white);
  color: var(--card-fg, black);
}

Thuộc tính tuỳ chỉnh được xác định trên :host chỉ có thể bị ghi đè bằng các quy tắc nhắm mục tiêu cụ thể đến phần tử lưu trữ. Để cho phép thuộc tính này kế thừa và có thể ghi đè từ bất kỳ vị trí nào trong cây, hãy để thuộc tính công khai không được đặt trên :host và dựa vào phương án dự phòng trong mỗi var(); không gieo thuộc tính công khai trên :host.

Để giữ cho phương án dự phòng được khai báo một lần thay vì lặp lại ở mỗi var(), hãy xác định một thuộc tính "bóng" riêng tư trên :host để đọc thuộc tính công khai và tham chiếu thuộc tính riêng tư trong nội bộ:

:host {
  --_card-bg: var(--card-bg, white);
  --_card-fg: var(--card-fg, black);
}
.card { background: var(--_card-bg); color: var(--_card-fg); }

Người dùng vẫn ghi đè --card-bg từ bên ngoài; dấu gạch dưới ở đầu cho biết bí danh là nội bộ và phương án dự phòng chỉ nằm ở một nơi.

::slotted(): tạo kiểu cho DOM sáng được chiếu

::slotted(selector) nhắm đến các nút cấp cao nhất được chỉ định cho một vùng. Nó không thể tiếp cận các phần tử con và thua bất kỳ quy tắc Light DOM nào (người dùng sở hữu nội dung của riêng họ), vì vậy, hãy sử dụng nó để chỉnh sửa nhẹ, không phải kiểm soát. ::slotted(...) !important có thể ghi đè các quy tắc của thành phần tiêu thụ, nhưng việc này sẽ ghi đè kiểu mà thành phần tiêu thụ có quyền sở hữu. Do đó, hãy chỉ dùng cho các bản sửa lỗi cấu trúc, chứ không phải ghi đè kiểu.

/* Matches a slotted <p>, but NOT a <p> nested inside a slotted <div>. */
::slotted(p) { margin: 0; }

::part() và exportparts: hiển thị các thành phần nội bộ để tạo giao diện

Đánh dấu một phần tử nội bộ bằng part="name" để cho phép người dùng tạo kiểu cho phần tử đó bằng bất kỳ thuộc tính nào mà không cần tìm hiểu cấu trúc nội bộ của bạn.

<!-- inside the shadow template -->
<button part="submit">Submit</button>
/* consumer stylesheet */
my-card::part(submit) { background: navy; color: white; }

Hiển thị một vài phần cấp cao (nút, dữ liệu đầu vào, nhãn), không phải mọi <div>; các bề mặt phần rộng trở thành một API dễ hỏng. Để hiển thị lại các phần của thành phần lồng nhau lên trên thông qua thành phần của riêng bạn, hãy chuyển tiếp các phần đó bằng exportparts:

<inner-widget exportparts="submit: card-submit"></inner-widget>

Hãy cân nhắc kỹ khi chọn giữa hai kênh: thuộc tính tuỳ chỉnh cho một tập hợp giá trị bị ràng buộc, an toàn cho hệ thống thiết kế; phần cho kiểu không bị hạn chế của một phần tử cụ thể.

Chuyển tiếp kiểu đến một phần tử gốc được gói

Khi một phần tử tuỳ chỉnh gói một thành phần điều khiển gốc (<my-button> xung quanh một <button> thực), có 3 điều thường khiến mọi người gặp khó khăn:

  • Các thành phần điều khiển biểu mẫu không kế thừa kiểu chữ. color và các thuộc tính tuỳ chỉnh vượt qua ranh giới, nhưng <button>, <input> và <select> lấy font và các thuộc tính khác từ biểu định kiểu UA, vốn không kế thừa. Do đó, thành phần điều khiển được gói sẽ hiển thị bằng phông chữ của nền tảng, chứ không phải phông chữ của trang, cho đến khi bạn đặt font: inherit (và thường là color: inherit) trên thành phần đó bên trong cây bóng. Đây là lỗi phổ biến nhất.
  • Kiểu :host không áp dụng cho phần tử bên trong. Việc đặt background/border/padding trên :host sẽ tạo kiểu cho hộp lưu trữ, chứ không phải thành phần điều khiển bên trong hộp đó. Chuyển tiếp có chủ đích: hiển thị chế độ kiểm soát dưới dạng ::part để tạo kiểu không giới hạn hoặc chuyển một tập hợp giá trị cố định thông qua các thuộc tính tuỳ chỉnh — quy tắc :host không xếp tầng vào phần tử được gói.
  • display: contents + all: inherit là một sự đánh đổi thực sự, chứ không phải là một lối tắt. Việc thu gọn máy chủ lưu trữ bằng display: contents và đặt lại chế độ kiểm soát bên trong bằng all: inherit hiện là cách chung duy nhất để truyền các kiểu tuỳ ý đến một phần tử gốc được gói mà không cần người dùng tìm hiểu các phần tử theo thành phần hoặc thuộc tính tuỳ chỉnh – và việc xoá hộp của chính máy chủ lưu trữ thường là điểm mấu chốt của mẫu (bố cục bù trừ cho một kế thừa tích hợp bị thiếu). Hai lưu ý thực tế: all: inherit xoá các giá trị mặc định của UA, vì vậy, hãy đặt phạm vi cho các thuộc tính cụ thể hoặc sử dụng revert khi bạn muốn khôi phục các giá trị mặc định; và trình chặn cụ thể — getBoundingClientRect() và getClientRects() trả về giá trị 0 cho các phần tử display: contents, vì vậy, vị trí neo so với máy chủ lưu trữ bị hỏng. Hãy sử dụng ::part hoặc thuộc tính tuỳ chỉnh khi những điểm đánh đổi đó không phù hợp.

:state(): tạo kiểu cho các trạng thái của thành phần

Một phần tử tuỳ chỉnh có thể hiển thị các trạng thái boolean nội bộ thông qua ElementInternals qua this.#internals.states.add('busy') và .delete('busy') (xem Phần tử tuỳ chỉnh để biết cách sử dụng vòng đời) và các trạng thái đó trở thành một kênh tạo kiểu trên ranh giới – như ::part và các thuộc tính tuỳ chỉnh, nhưng cho trạng thái thay vì cấu trúc hoặc giá trị. So khớp các phần tử đó bên trong cây bóng bằng :host(:state(name)) và từ biểu định kiểu của người dùng bằng tag-name:state(name):

/* Inside the component's shadow tree. */
:host(:state(busy)) button { opacity: 0.6; pointer-events: none; }

/* From the consumer's stylesheet — a read-only public hook. */
favorite-button:state(favorited) { filter: drop-shadow(0 0 4px crimson); }

Không giống như thuộc tính được phản ánh, trạng thái tuỳ chỉnh là chỉ có thể đọc đối với bên ngoài: người dùng có thể tạo kiểu cho trạng thái đó nhưng không thể đặt hoặc giả mạo trạng thái đó, vì vậy thành phần này vẫn là trình ghi duy nhất. Hãy ưu tiên sử dụng thuộc tính này thay vì thuộc tính data-* bất cứ khi nào trạng thái thuộc sở hữu của thành phần và chỉ cần được tạo kiểu.

@layer và truy vấn vùng chứa bên trong các thành phần

  • Lớp xếp tầng (@layer) hoạt động bên trong một gốc bóng và được đặt trong phạm vi của gốc bóng đó; tên lớp trong cây bóng độc lập với lớp có cùng tên trong trang. Hãy sử dụng các lớp này để sắp xếp kiểu thành phần của riêng bạn (ví dụ: @layer base, theme;) để các thuộc tính tuỳ chỉnh và ::part mà người dùng ghi đè vẫn hoạt động như dự kiến.
  • Truy vấn vùng chứa là công cụ phù hợp cho các thành phần thích ứng: một thành phần phải phản hồi không gian được cung cấp chứ không phải khung nhìn. Khai báo máy chủ lưu trữ hoặc trình bao bọc dưới dạng vùng chứa (cung cấp cho :host một hộp bố cục như display: block, vì các phần tử tuỳ chỉnh mặc định là display: inline và vùng chứa kích thước không áp dụng cho các phần tử inline hoặc contents) và truy vấn vùng chứa đó, để cùng một thành phần thích ứng trong thanh bên hoặc vùng có chiều rộng đầy đủ.
:host {
  display: block; /* Required: size containment does not apply to default display: inline */
  container-type: inline-size;
}

@container (min-width: 30rem) {
  .card { grid-template-columns: 1fr 2fr; }
}

Chiến lược dự phòng

::part, lớp xếp tầng và truy vấn vùng chứa được cung cấp rộng rãi; giao diện chủ yếu thông qua các thuộc tính tuỳ chỉnh kế thừa, hoạt động ngay cả khi các thuộc tính này không hoạt động. Như đã lưu ý ở trên, đừng dựa vào :host-context() – đây là lựa chọn chỉ dành cho Chromium và không phải là lựa chọn hướng tới tương lai.