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

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

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: behind a flag.
  • Safari: 17.4.

Source

ศิลปะอันละเอียดอ่อนในการเขียนตัวเลือก CSS

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

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

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

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

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

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

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

ขอแนะนำ @scope

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

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

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

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

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

@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) { ... }

โปรดทราบว่ากฎสไตล์ที่มีขอบเขตจะหนีออกจากซับต้นไม้ไม่ได้ การเลือกอย่าง :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 ภายในขอบเขตโดนัท color จะยังคงรับค่าจากองค์ประกอบย่อยภายในรูของโดนัท

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

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

(ภาพปกโดย rustam burkhanov ใน Unsplash)