จำกัดการเข้าถึงของตัวเลือกด้วยกฎ @scope ของ CSS

ดูวิธีใช้ @scope เพื่อเลือกองค์ประกอบภายใน Subtree ที่จำกัดของ DOM เท่านั้น

เผยแพร่: 4 ตุลาคม 2023

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 146.
  • Safari: 17.4.

Source

เมื่อเขียนตัวเลือก คุณอาจพบว่าตัวเองอยู่ระหว่าง 2 โลก ในอีกด้านหนึ่ง คุณก็ต้องการระบุองค์ประกอบที่เลือกอย่างชัดเจน ในทางกลับกัน คุณต้องการให้ตัวเลือกยังคงลบล้างได้ง่ายและไม่เชื่อมโยงกับโครงสร้าง DOM อย่างแน่นหนา

เช่น เมื่อต้องการเลือก "รูปภาพฮีโร่ในพื้นที่เนื้อหาของคอมโพเนนต์การ์ด" ซึ่งเป็นการเลือกองค์ประกอบที่ค่อนข้างเฉพาะเจาะจง คุณคงไม่ต้องการเขียนตัวเลือกอย่าง .card > .content > img.hero

  • ตัวเลือกนี้มีความเฉพาะเจาะจงค่อนข้างสูงที่ (0,3,1) ซึ่งทำให้การลบล้างเป็นเรื่องยากเมื่อโค้ดของคุณเติบโตขึ้น
  • การใช้ตัวเลือกองค์ประกอบย่อยโดยตรงทำให้เชื่อมโยงกับโครงสร้าง DOM อย่างใกล้ชิด หากมาร์กอัปมีการเปลี่ยนแปลง คุณจะต้องเปลี่ยน CSS ด้วย

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

การหาสมดุลที่เหมาะสมในเรื่องนี้มักเป็นเรื่องที่ท้าทาย ในช่วงหลายปีที่ผ่านมา นักพัฒนาแอปบางรายได้คิดค้นโซลูชันและวิธีแก้ปัญหาเบื้องต้นเพื่อช่วยคุณในสถานการณ์เช่นนี้ เช่น

  • วิธีการต่างๆ เช่น BEM กำหนดให้คุณกำหนดคลาส card__img card__img--hero ให้กับองค์ประกอบนั้นเพื่อคงความเฉพาะเจาะจงไว้ในระดับต่ำ ขณะเดียวกันก็ช่วยให้คุณเลือกได้อย่างเฉพาะเจาะจง
  • โซลูชันที่ใช้ JavaScript เช่น Scoped CSS หรือ Styled Components จะเขียนตัวเลือกทั้งหมดใหม่โดยการเพิ่มสตริงที่สร้างขึ้นแบบสุ่ม เช่น sc-596d7e0e-4 ลงในตัวเลือกเพื่อป้องกันไม่ให้ตัวเลือกกำหนดเป้าหมายองค์ประกอบที่อีกด้านหนึ่งของหน้าเว็บ
  • ห้องสมุดบางแห่งถึงกับยกเลิกตัวเลือกทั้งหมดและกำหนดให้คุณใส่ทริกเกอร์การจัดรูปแบบในมาร์กอัปโดยตรง

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

ขอแนะนำ @scope

@scope ช่วยให้คุณจำกัดการเข้าถึงของตัวเลือกได้ โดยการตั้งค่ารูทการกำหนดขอบเขต ซึ่งจะเป็นตัวกำหนดขอบเขตบนของ Subtree ที่คุณต้องการกำหนดเป้าหมาย เมื่อตั้งค่ารูทการกำหนดขอบเขตแล้ว กฎสไตล์ที่อยู่ในรูทดังกล่าว ซึ่งเรียกว่ากฎสไตล์ที่กำหนดขอบเขต จะเลือกได้จากซับทรีที่จำกัดของ DOM เท่านั้น

เช่น หากต้องการกำหนดเป้าหมายเฉพาะองค์ประกอบ <img> ในคอมโพเนนต์ .card คุณจะต้องตั้งค่า .card เป็นรูทการกำหนดขอบเขตของกฎ @ @scope

@scope (.card) {
    img {
        border-color: green;
    }
}

กฎรูปแบบที่กำหนดขอบเขต img { … } จะเลือกได้เฉพาะองค์ประกอบ <img> ที่อยู่ในขอบเขตขององค์ประกอบ .card ที่ตรงกันเท่านั้น

หากต้องการป้องกันไม่ให้เลือกองค์ประกอบ <img> ภายในพื้นที่เนื้อหาของการ์ด (.card__content) คุณสามารถทำให้ตัวเลือก img มีความเฉพาะเจาะจงมากขึ้นได้ อีกวิธีหนึ่งในการทำเช่นนี้คือการใช้ข้อเท็จจริงที่ว่า@scopeกฎ @ ยังยอมรับขีดจำกัดการกำหนดขอบเขตซึ่งกำหนดขอบเขตล่างด้วย

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

กฎรูปแบบที่กำหนดขอบเขตนี้จะกำหนดเป้าหมายเฉพาะองค์ประกอบ <img> ที่วางอยู่ระหว่างองค์ประกอบ .card และ .card__content ในแผนผังบรรพบุรุษ การกำหนดขอบเขตประเภทนี้ที่มีขอบเขตบนและล่างมักเรียกว่าขอบเขตโดนัท

ตัวเลือก :scope

โดยค่าเริ่มต้น กฎการจัดรูปแบบที่กำหนดขอบเขตทั้งหมดจะสัมพันธ์กับรูทการกำหนดขอบเขต นอกจากนี้ คุณยังกำหนดเป้าหมายไปยังองค์ประกอบรูทของการกำหนดขอบเขตได้ด้วย โดยใช้:scopeตัวเลือก

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

ตัวเลือกภายในกฎสไตล์ที่กำหนดขอบเขตจะได้รับ :scope ที่นำหน้าโดยนัย หากต้องการ คุณสามารถระบุอย่างชัดเจนได้โดยการใส่ :scope ด้วยตัวเอง หรือจะเพิ่มตัวเลือก & ไว้ข้างหน้าจากการซ้อน CSS ก็ได้

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

ขีดจํากัดการกําหนดขอบเขตสามารถใช้:scopeคลาสเสมือนเพื่อกําหนดความสัมพันธ์ที่เฉพาะเจาะจงกับรูทการกําหนดขอบเขตได้

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

ขีดจำกัดการกำหนดขอบเขตยังอ้างอิงองค์ประกอบนอกรูทการกำหนดขอบเขตได้ด้วยการใช้ :scope เช่น

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

กฎสไตล์ที่กำหนดขอบเขตเองจะหลุดออกจาก Subtree ไม่ได้ การเลือกอย่าง :scope + p ไม่ถูกต้องเนื่องจากพยายามเลือกองค์ประกอบที่ไม่อยู่ในขอบเขต

@scope และความเฉพาะเจาะจง

ตัวเลือกที่คุณใช้ในส่วนต้นของ @scope จะไม่ส่งผลต่อความเฉพาะเจาะจงของตัวเลือกที่อยู่ในนั้น ในตัวอย่างของเรา ความเฉพาะเจาะจงของimgตัวเลือกยังคงเป็น (0,0,1)

@scope (#sidebar) {
  img { /* Specificity = (0,0,1) */
    ...
  }
}

ความเฉพาะเจาะจงของ :scope คือความเฉพาะเจาะจงของคลาสเสมือนปกติ ซึ่งก็คือ (0,1,0)

@scope (#sidebar) {
  :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
    ...
  }
}

ในตัวอย่างต่อไปนี้ ภายใน & จะได้รับการเขียนใหม่เป็นตัวเลือกที่ใช้สำหรับรูทการกำหนดขอบเขต ซึ่งห่อหุ้มอยู่ภายในตัวเลือก :is() ในท้ายที่สุด เบราว์เซอร์จะใช้ :is(#sidebar, .card) img เป็นตัวเลือกเพื่อทำการจับคู่ กระบวนการนี้เรียกว่าการลดความซับซ้อน

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        ...
    }
}

เนื่องจาก & จะได้รับการยกเลิกการน้ำตาลโดยใช้ :is() ระบบจะคำนวณความเฉพาะเจาะจงของ & ตามกฎความเฉพาะเจาะจงของ :is() ซึ่งความเฉพาะเจาะจงของ & คือความเฉพาะเจาะจงของอาร์กิวเมนต์ที่เฉพาะเจาะจงที่สุด

เมื่อใช้กับตัวอย่างนี้ ความเฉพาะเจาะจงของ :is(#sidebar, .card) คือความเฉพาะเจาะจงของอาร์กิวเมนต์ที่เฉพาะเจาะจงที่สุด ซึ่งก็คือ #sidebar ดังนั้นจึงกลายเป็น (1,0,0) เมื่อรวมเข้ากับความเฉพาะเจาะจงของ img ซึ่งก็คือ (0,0,1) คุณจะได้ (1,0,1) เป็นความเฉพาะเจาะจงของตัวเลือกที่ซับซ้อนทั้งหมด

@scope (#sidebar, .card) {
  & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
    ...
  }
}

ความแตกต่างระหว่าง :scope กับ & ใน @scope

นอกเหนือจากความแตกต่างในวิธีคำนวณความเฉพาะเจาะจงแล้ว ความแตกต่างอีกอย่างระหว่าง :scope กับ & คือ :scope แสดงถึงรูทการกำหนดขอบเขตที่ตรงกัน ในขณะที่ & แสดงถึงตัวเลือกที่ใช้เพื่อจับคู่รูทการกำหนดขอบเขต

ด้วยเหตุนี้ คุณจึงใช้ & ได้หลายครั้ง ซึ่งแตกต่างจาก :scope ที่คุณใช้ได้เพียงครั้งเดียว เนื่องจากคุณไม่สามารถจับคู่รูทการกำหนดขอบเขตภายในรูทการกำหนดขอบเขตได้

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    
  }
}

ขอบเขตที่ไม่มีส่วนนำ

เมื่อเขียนรูปแบบอินไลน์ด้วยองค์ประกอบ <style> คุณสามารถกำหนดขอบเขตกฎรูปแบบไปยังองค์ประกอบหลักที่ล้อมรอบขององค์ประกอบ <style> ได้โดยไม่ต้องระบุรูทการกำหนดขอบเขต โดยทำได้ด้วยการละเว้นส่วนนำของ @scope

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

ในตัวอย่างข้างต้น กฎที่กำหนดขอบเขตจะกำหนดเป้าหมายเฉพาะองค์ประกอบภายใน div ที่มีชื่อคลาส card__header เนื่องจาก div นั้นเป็นองค์ประกอบระดับบนขององค์ประกอบ <style>

@scope ในการเรียงซ้อน

ภายใน CSS Cascade @scope ยังเพิ่มเกณฑ์ใหม่ด้วย นั่นคือความใกล้เคียงของการกำหนดขอบเขต ขั้นตอนนี้จะอยู่หลังความเฉพาะเจาะจงแต่ก่อนลำดับการปรากฏ

การแสดงภาพของ CSS Cascade

ตามข้อกำหนด

เมื่อเปรียบเทียบประกาศที่ปรากฏในกฎสไตล์ที่มีรูทการกำหนดขอบเขตที่แตกต่างกัน ประกาศที่มีการข้ามองค์ประกอบรุ่นหรือองค์ประกอบร่วมกันน้อยที่สุดระหว่างรูทการกำหนดขอบเขตกับออบเจ็กต์กฎสไตล์ที่กำหนดขอบเขตจะเป็นผู้ชนะ

ขั้นตอนใหม่นี้มีประโยชน์เมื่อซ้อนตัวแปรหลายรายการของคอมโพเนนต์ ลองดูตัวอย่างนี้ที่ยังไม่ได้ใช้ @scope

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

เมื่อดูมาร์กอัปเล็กๆ นั้น ลิงก์ที่ 3 จะเป็น white แทนที่จะเป็น black แม้ว่าจะเป็นองค์ประกอบย่อยของ div ที่มีคลาส .light อยู่ก็ตาม เนื่องจากเกณฑ์ลำดับการปรากฏซึ่งแคสเคดใช้ที่นี่เพื่อพิจารณาผู้ชนะ ระบบจะเห็นว่ามีการประกาศ .dark a เป็นรายการสุดท้าย จึงชนะจากกฎ .light a

ตอนนี้ปัญหาต่อไปนี้ได้รับการแก้ไขแล้วด้วยเกณฑ์ความใกล้เคียงในการกำหนดขอบเขต

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

เนื่องจากตัวเลือก a ที่กำหนดขอบเขตทั้ง 2 รายการมีความเฉพาะเจาะจงเหมือนกัน เกณฑ์ความใกล้เคียงในการกำหนดขอบเขตจึงเริ่มทำงาน โดยจะพิจารณาทั้ง 2 ตัวเลือกตามความใกล้เคียงกับรูทการกำหนดขอบเขต สำหรับองค์ประกอบ a รายการที่ 3 จะมีเพียง 1 ฮ็อปไปยังรูทการกำหนดขอบเขต .light แต่มี 2 ฮ็อปไปยังรูท .dark ดังนั้น ตัวเลือก a ใน .light จะชนะ

การแยกตัวเลือก ไม่ใช่การแยกสไตล์

โปรดทราบว่า @scope จะจำกัดการเข้าถึงของตัวเลือก โดยจะไม่มีการแยกสไตล์ พร็อพเพอร์ตี้ที่รับช่วงลงไปยังพร็อพเพอร์ตี้ย่อยจะยังคงรับช่วงต่อไป แม้จะอยู่นอกขอบเขตล่างของ @scope พร็อพเพอร์ตี้ดังกล่าวคือพร็อพเพอร์ตี้ color เมื่อ ประกาศว่ามีอยู่ภายในขอบเขตโดนัท color จะยังคงสืบทอดลงไป ยังองค์ประกอบย่อยภายในรูของโดนัท

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

ในตัวอย่างนี้ องค์ประกอบ .card__content และองค์ประกอบย่อยมีสี hotpink เนื่องจากรับค่ามาจาก .card