การจัดรูปแบบคอมโพเนนต์เว็บ

Shadow DOM เป็นขอบเขตของสไตล์ โดยกฎของหน้าเว็บจะไม่เข้าถึง และกฎของคอมโพเนนต์จะไม่รั่วไหลออกไป ยกเว้นพร็อพเพอร์ตี้ที่รับค่ามา (รวมถึงคุณสมบัติที่กำหนดเอง) ข้ามเส้นอย่างตั้งใจผ่านช่องทางด้านล่าง

: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; }

หลีกเลี่ยง :host-context() ฟีเจอร์นี้จะเข้าถึงภายนอกขอบเขตเพื่อทดสอบระดับบน แต่ Firefox และ Safari ปฏิเสธที่จะจัดส่ง และ CSSWG ก็มีแนวโน้มที่จะไม่พัฒนาฟีเจอร์นี้ต่อ โดยถือว่าเป็นทางตันสำหรับ Chromium เท่านั้น ไม่ใช่ตัวเลือกที่มองไปข้างหน้า ธีมผ่านคุณสมบัติที่กำหนดเองของ CSS ที่รับค่ามาแทน (ด้านล่าง): ผู้บริโภคตั้งค่าคุณสมบัติในระดับบนและคอมโพเนนต์จะอ่านค่าข้ามขอบเขต ซึ่งใช้ได้ทุกที่และทำให้ทั้ง 2 ฝั่งแยกกัน

พร็อพเพอร์ตี้ที่รับช่วงมาและคุณสมบัติที่กำหนดเองจะข้ามขอบเขต

พร็อพเพอร์ตี้ CSS ที่รับค่ามา (color, font-family, line-height และพร็อพเพอร์ตี้ที่กำหนดเองของ CSS) จะส่งผ่านขอบเขต Shadow ซึ่งทำให้คุณสมบัติที่กำหนดเองเป็น API การกำหนดธีมหลัก โดยแสดงคุณสมบัติเหล่านั้นด้วยทางเลือกที่เหมาะสม

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

คุณสมบัติที่กำหนดเองซึ่งกำหนดใน :host จะถูกลบล้างได้โดยกฎที่กำหนดเป้าหมายองค์ประกอบโฮสต์โดยเฉพาะเท่านั้น หากต้องการให้รับค่าและลบล้างได้จากทุกที่ในลำดับชั้น ให้ปล่อยพร็อพเพอร์ตี้สาธารณะใน :host ไว้โดยไม่ต้องตั้งค่า และใช้ค่าสำรองใน var() แต่ละรายการ อย่าสร้างพร็อพเพอร์ตี้สาธารณะใน :host

หากต้องการให้ประกาศโฆษณาสำรองเพียงครั้งเดียวแทนที่จะประกาศซ้ำในทุก var() ให้กำหนดพร็อพเพอร์ตี้ "Shadow" ส่วนตัวใน :host ที่อ่านพร็อพเพอร์ตี้สาธารณะ และอ้างอิงพร็อพเพอร์ตี้ส่วนตัวภายใน ดังนี้

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

ผู้บริโภคยังคงลบล้าง --card-bg จากภายนอกได้ ขีดล่างนำหน้าจะส่งสัญญาณว่าชื่อแทนเป็นแบบภายใน และฟังก์ชันสำรองจะอยู่ในที่เดียวเท่านั้น

::slotted(): การจัดรูปแบบ Light DOM ที่ฉาย

::slotted(selector) กำหนดเป้าหมายโหนดระดับบนสุดที่กำหนดให้กับช่อง โดยไม่สามารถเข้าถึงองค์ประกอบสืบทอด และจะแพ้กฎ Light DOM (ผู้บริโภคเป็นเจ้าของเนื้อหาของตนเอง) ดังนั้นให้ใช้เพื่อการปรับแต่งเล็กน้อย ไม่ใช่การควบคุม ::slotted(...) !important สามารถเอาชนะกฎของผู้บริโภคได้ แต่การทำเช่นนั้นจะลบล้างการจัดรูปแบบที่ผู้บริโภคมีสิทธิ์เป็นเจ้าของ โปรดสงวนไว้สำหรับการแก้ไขโครงสร้าง ไม่ใช่การลบล้างด้านความสวยงาม

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

::part() และ exportparts: แสดงข้อมูลภายในสำหรับการกำหนดธีม

ทำเครื่องหมายองค์ประกอบภายในด้วย part="name" เพื่อให้ผู้บริโภคจัดรูปแบบด้วยพร็อพเพอร์ตี้ใดก็ได้ โดยไม่ต้องเรียนรู้โครงสร้างภายในของคุณ

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

แสดงส่วนระดับสูงบางส่วน (ปุ่ม, อินพุต, ป้ายกำกับ) ไม่ใช่ทุก <div>; พื้นผิวส่วนที่กว้างจะกลายเป็น API ที่เปราะบาง หากต้องการแสดงส่วนต่างๆ ของคอมโพเนนต์ที่ฝังไว้ขึ้นไปด้านบนผ่านคอมโพเนนต์ของคุณเอง ให้ส่งต่อส่วนเหล่านั้นด้วย exportparts ดังนี้

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

เลือกช่องทางใดช่องทางหนึ่งอย่างรอบคอบ ได้แก่ คุณสมบัติที่กำหนดเองสำหรับชุดค่าที่จำกัดและปลอดภัยสำหรับระบบการออกแบบ หรือส่วนต่างๆ สำหรับการจัดรูปแบบองค์ประกอบที่เฉพาะเจาะจงแบบไม่จำกัด

การส่งต่อสไตล์ไปยังองค์ประกอบเนทีฟที่รวมไว้

เมื่อองค์ประกอบที่กำหนดเองรวมการควบคุมแบบเนทีฟ (<my-button> รอบ <button> จริง) มักจะมี 3 สิ่งที่ทำให้ผู้ใช้สับสน ได้แก่

  • การควบคุมแบบฟอร์มจะไม่รับช่วงการจัดพิมพ์ color และคุณสมบัติที่กำหนดเองจะข้ามขอบเขต แต่ <button>, <input> และ <select> จะใช้ font และองค์ประกอบที่เกี่ยวข้องจากสไตล์ชีต UA ซึ่งไม่รับช่วงต่อ ดังนั้น ตัวควบคุมที่รวมไว้จึงแสดงผลในแบบอักษรของแพลตฟอร์ม ไม่ใช่แบบอักษรของหน้าเว็บ จนกว่าคุณจะตั้งค่า font: inherit (และโดยปกติคือ color: inherit) ในตัวควบคุมภายใน Shadow Tree นี่เป็นข้อผิดพลาดที่พบบ่อยที่สุดเพียงข้อเดียว
  • รูปแบบ :host ไม่เข้าถึงองค์ประกอบภายใน การตั้งค่า background/border/padding ใน :host จะจัดรูปแบบกล่องโฮสต์ ไม่ใช่การควบคุมภายในกล่อง ส่งต่ออย่างรอบคอบ: แสดงการควบคุมเป็น ::part สำหรับการจัดรูปแบบปลายเปิด หรือส่งชุดค่าคงที่ผ่านคุณสมบัติที่กำหนดเอง — กฎ :host จะไม่เรียงซ้อนกันในองค์ประกอบที่รวมอยู่
  • display: contents + all: inherit เป็นการแลกเปลี่ยนที่แท้จริง ไม่ใช่ทางลัด ปัจจุบันการยุบโฮสต์ด้วย display: contents และการรีเซ็ตการควบคุมภายในด้วย all: inherit เป็นวิธีทั่วไปเพียงวิธีเดียวในการส่งผ่านสไตล์ที่กำหนดเองไปยังองค์ประกอบเนทีฟที่รวมไว้โดยที่ผู้ใช้ไม่ต้องเรียนรู้ส่วนประกอบต่อคอมโพเนนต์หรือคุณสมบัติที่กำหนดเอง และการนำกล่องของโฮสต์ออกมักจะเป็นจุดประสงค์ของรูปแบบ (องค์ประกอบที่ชดเชยการสืบทอดในตัวที่ขาดหายไป) ข้อควรระวังที่แท้จริง 2 ข้อคือ all: inherit จะล้างค่าเริ่มต้นของ UA ดังนั้นให้กำหนดขอบเขตเป็นพร็อพเพอร์ตี้ที่เฉพาะเจาะจงหรือใช้ revert เมื่อต้องการให้ค่าเริ่มต้นกลับมา และตัวบล็อกที่เป็นรูปธรรมคือ getBoundingClientRect() และ getClientRects() จะแสดงผลเป็น 0 สำหรับองค์ประกอบ display: contents ดังนั้นการกำหนดตำแหน่ง Anchor เทียบกับโฮสต์จึงใช้งานไม่ได้ ใช้ ::part หรือคุณสมบัติที่กำหนดเองเมื่อข้อดีข้อเสียเหล่านั้นไม่เหมาะสม

:state(): จัดรูปแบบสถานะของคอมโพเนนต์เอง

องค์ประกอบที่กำหนดเองสามารถแสดงสถานะบูลีนภายในผ่าน ElementInternals ผ่าน this.#internals.states.add('busy') และ .delete('busy') (ดูองค์ประกอบที่กำหนดเองสำหรับการใช้งานวงจร) และสถานะเหล่านั้นจะกลายเป็นช่องทางการจัดรูปแบบข้ามขอบเขต เช่น ::part และคุณสมบัติที่กำหนดเอง แต่สำหรับสถานะแทนที่จะเป็นโครงสร้างหรือค่า จับคู่องค์ประกอบเหล่านั้นภายใน Shadow Tree ด้วย :host(:state(name)) และจากสไตล์ชีตของผู้ใช้ด้วย 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); }

สถานะที่กำหนดเองจะอ่านอย่างเดียวจากภายนอก ซึ่งต่างจากแอตทริบิวต์ที่แสดงผล กล่าวคือ ผู้ใช้สามารถจัดรูปแบบได้ แต่จะตั้งค่าหรือปลอมแปลงไม่ได้ ดังนั้นคอมโพเนนต์จึงยังคงเป็นผู้เขียนเพียงรายเดียว ใช้แอตทริบิวต์นี้แทนแอตทริบิวต์ data-* เมื่อคอมโพเนนต์เป็นเจ้าของสถานะและต้องจัดรูปแบบเท่านั้น

@layer และการค้นหาคอนเทนเนอร์ภายในคอมโพเนนต์

  • เลเยอร์แบบเรียงซ้อน (@layer) ทำงานภายใน Shadow Root และกำหนดขอบเขตไว้ที่ Shadow Root ชื่อเลเยอร์ใน Shadow Tree จะเป็นอิสระจากเลเยอร์ที่มีชื่อเดียวกันในหน้าเว็บ ใช้เพื่อจัดลำดับสไตล์คอมโพเนนต์ของคุณเอง (เช่น @layer base, theme;) เพื่อให้การลบล้างของผู้บริโภคผ่านคุณสมบัติที่กำหนดเองและ ::part ยังคงชนะตามที่คาดการณ์ไว้
  • การค้นหาคอนเทนเนอร์เป็นเครื่องมือที่เหมาะสำหรับคอมโพเนนต์ที่ปรับเปลี่ยนตามอุปกรณ์ โดยคอมโพเนนต์ควรตอบสนองต่อพื้นที่ที่ได้รับ ไม่ใช่พอร์ตการมองเห็น ประกาศโฮสต์หรือ Wrapper เป็นคอนเทนเนอร์ (ให้ :host เป็นกล่องเลย์เอาต์ เช่น display: block เนื่องจากองค์ประกอบที่กำหนดเองมีค่าเริ่มต้นเป็น display: inline และการจำกัดขนาดใช้ไม่ได้กับองค์ประกอบ inline หรือ contents) และค้นหาองค์ประกอบดังกล่าว เพื่อให้คอมโพเนนต์เดียวกันปรับเปลี่ยนในแถบด้านข้างหรือพื้นที่แบบเต็มความกว้าง
: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; }
}

กลยุทธ์สำรอง

::part, เลเยอร์ Cascade และการค้นหาคอนเทนเนอร์พร้อมใช้งานอย่างกว้างขวาง ธีมส่วนใหญ่จะใช้พร็อพเพอร์ตี้ที่กำหนดเองที่รับค่ามา ซึ่งจะทำงานได้แม้ว่าสิ่งเหล่านี้จะไม่ทำงานก็ตาม ตามที่ระบุไว้ข้างต้น อย่าใช้ :host-context() เนื่องจากใช้ได้กับ Chromium เท่านั้นและไม่ใช่ตัวเลือกที่เหมาะสำหรับอนาคต