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

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

การรองรับเบราว์เซอร์

  • Chrome: 118.
  • ขอบ: 118
  • Firefox: อยู่หลังธง
  • Safari: 17.4

แหล่งที่มา

ศิลปะในการเขียนตัวเลือก 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 เพื่อจำกัดการเข้าถึงของตัวเลือกได้ โดยการตั้งค่ารูทที่กำหนดขอบเขตซึ่งกำหนดขอบเขตด้านบนของแผนผังย่อยที่ต้องการกำหนดเป้าหมาย เมื่อมีชุดรูทที่กำหนดขอบเขต กฎสไตล์ที่มีอยู่ซึ่งมีชื่อว่ากฎรูปแบบที่กำหนดขอบเขต จะเลือกได้จากแผนผังย่อยแบบจำกัดของ DOM เท่านั้น

ตัวอย่างเช่น หากต้องการกำหนดเป้าหมายเฉพาะองค์ประกอบ <img> ในคอมโพเนนต์ .card คุณต้องกำหนด .card เป็นรากที่กำหนดขอบเขตของกฎ at @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 Nest ก็ได้

@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 ใน Cascade

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

การแสดงภาพการเรียงซ้อน CSS

ตามข้อกําหนด

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

ขั้นตอนใหม่นี้เหมาะสำหรับการซ้อนคอมโพเนนต์หลายรูปแบบ ลองดูตัวอย่างนี้ ที่ยังไม่ได้ใช้ @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>

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

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

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

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

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

หมายเหตุสรุป: การแยกตัวเลือก ไม่ใช่การแยกรูปแบบ

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

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

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

(ภาพหน้าปกโดย rustam burkhanov บน Unsplash)