การสร้างคอมโพเนนต์การเปลี่ยนธีม

ภาพรวมพื้นฐานเกี่ยวกับวิธีสร้างคอมโพเนนต์สวิตช์ธีมแบบปรับเปลี่ยนได้และเข้าถึงได้

ในโพสต์นี้ เราต้องการแชร์แนวคิดเกี่ยวกับวิธีสร้างคอมโพเนนต์สวิตช์ธีมมืดและธีมสว่าง ลองใช้เดโม

เพิ่มขนาดปุ่มสาธิตเพื่อให้มองเห็นได้ง่าย

หากต้องการดูวิดีโอ โปรดดูโพสต์เวอร์ชัน YouTube ที่นี่

ภาพรวม

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

การสร้างฟีเจอร์นี้ต้องคำนึงถึงวิศวกรรมเว็บหลายประการ ตัวอย่างเช่น ควรแจ้งให้เบราว์เซอร์ทราบถึงค่ากําหนดโดยเร็วที่สุดเพื่อป้องกันไม่ให้สีของหน้าเว็บกะพริบ และการควบคุมต้องซิงค์กับระบบก่อน จากนั้นจึงอนุญาตข้อยกเว้นที่เก็บไว้ฝั่งไคลเอ็นต์

แผนภาพแสดงตัวอย่างการโหลดหน้าเว็บ JavaScript และเหตุการณ์การโต้ตอบกับเอกสารเพื่อแสดงให้เห็นภาพรวมว่ามี 4 เส้นทางในการตั้งค่าธีม

Markup

คุณควรใช้ <button> สําหรับปุ่มเปิด/ปิด เนื่องจากคุณจะได้ประโยชน์จากเหตุการณ์และการโต้ตอบของฟีเจอร์ต่างๆ ที่เบราว์เซอร์มีให้ เช่น เหตุการณ์การคลิกและความสามารถในการโฟกัส

ปุ่ม

ปุ่มต้องมีคลาสสําหรับใช้จาก CSS และรหัสสําหรับใช้จาก JavaScript นอกจากนี้ เนื่องจากเนื้อหาของปุ่มเป็นไอคอนแทนข้อความ ให้เพิ่มแอตทริบิวต์ชื่อเพื่อระบุข้อมูลเกี่ยวกับวัตถุประสงค์ของปุ่ม สุดท้าย ให้เพิ่ม [aria-label] เพื่อเก็บสถานะของปุ่มไอคอนไว้ เพื่อให้โปรแกรมอ่านหน้าจอแชร์สถานะของธีมกับผู้ที่มีความบกพร่องทางสายตาได้

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-label และ aria-live สุภาพ

หากต้องการระบุว่าควรประกาศการเปลี่ยนแปลง aria-label ให้กับโปรแกรมอ่านหน้าจอ ให้เพิ่ม aria-live="polite" ลงในปุ่ม

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

การเพิ่มมาร์กอัปนี้จะเป็นสัญญาณให้โปรแกรมอ่านหน้าจอบอกผู้ใช้ถึงสิ่งที่เปลี่ยนแปลงไปอย่างสุภาพแทนที่จะใช้ aria-live="assertive" ในกรณีของปุ่มนี้ อุปกรณ์จะอ่านออกเสียงว่า "สว่าง" หรือ "มืด" โดยขึ้นอยู่กับสถานะของ aria-label

ไอคอนกราฟิกเวกเตอร์ที่ปรับขนาดได้ (SVG)

SVG เป็นวิธีสร้างรูปร่างที่ปรับขนาดได้และมีคุณภาพสูงโดยใช้มาร์กอัปเพียงเล็กน้อย การโต้ตอบกับปุ่มสามารถทริกเกอร์สถานะภาพใหม่สำหรับเวกเตอร์ได้ ทำให้ SVG เหมาะสําหรับไอคอน

มาร์กอัป SVG ต่อไปนี้จะอยู่ภายใน <button>

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

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

ดวงอาทิตย์

ไอคอนดวงอาทิตย์ที่แสดงพร้อมกับแสงแดดที่เลือนหายไปและลูกศรสีชมพูร้อนชี้ไปที่วงกลมตรงกลาง

กราฟิกดวงอาทิตย์ประกอบด้วยวงกลมและเส้น ซึ่ง SVG มีรูปร่างที่สะดวก <circle> จะอยู่ตรงกลางโดยการตั้งค่าพร็อพเพอร์ตี้ cx และ cy เป็น 12 ซึ่งเท่ากับครึ่งหนึ่งของขนาดวิวพอร์ต (24) จากนั้นกำหนดรัศมี (r) เป็น 6 ซึ่งจะเป็นการกำหนดขนาด

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

นอกจากนี้ พร็อพเพอร์ตี้มาสก์ยังชี้ไปยังรหัสขององค์ประกอบ SVG ซึ่งคุณจะสร้างในลำดับถัดไป และสุดท้ายให้สีเติมที่ตรงกับสีข้อความของหน้าเว็บด้วย currentColor

แสงอาทิตย์

ไอคอนดวงอาทิตย์ที่แสดงโดยมีดวงอาทิตย์ตรงกลางจางลงและลูกศรสีชมพูร้อนชี้ไปที่แสงอาทิตย์

ถัดไป ให้เพิ่มเส้นแสงอาทิตย์ใต้วงกลมภายในกลุ่มองค์ประกอบ <g>

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

ในครั้งนี้ ระบบจะตั้งค่าเส้นของแต่ละเส้นแทนที่จะตั้งค่าfill เป็นcurrentColor เส้นและรูปวงกลมสร้างดวงอาทิตย์ที่มีแสงส่องลงมาได้สวยงาม

ดวงจันทร์

เพื่อสร้างภาพลวงตาของการเปลี่ยนจากสว่าง (ดวงอาทิตย์) เป็นมืด (ดวงจันทร์) อย่างราบรื่น ดวงจันทร์จึงเป็นการขยายไอคอนดวงอาทิตย์โดยใช้มาสก์ SVG

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
กราฟิกที่มีเลเยอร์แนวตั้ง 3 เลเยอร์เพื่อช่วยแสดงวิธีการทํางานของการมาสก์ เลเยอร์ด้านบนเป็นรูปสี่เหลี่ยมจัตุรัสสีขาวที่มีวงกลมสีดํา เลเยอร์กลางคือไอคอนดวงอาทิตย์
เลเยอร์ด้านล่างติดป้ายกำกับว่า &quot;ผลลัพธ์&quot; และแสดงไอคอนดวงอาทิตย์ที่มีการตัดออกตรงตำแหน่งวงกลมสีดำของเลเยอร์ด้านบน

มาสก์ที่ใช้ SVG มีประสิทธิภาพสูง เนื่องจากใช้สีขาวและสีดำเพื่อนำส่วนต่างๆ ของกราฟิกอื่นออกหรือรวมไว้ได้ ไอคอนดวงอาทิตย์จะบดบังด้วยรูปร่างดวงจันทร์ <circle> ที่มีมาสก์ SVG โดยเพียงแค่ย้ายรูปร่างวงกลมเข้าและออกจากพื้นที่มาสก์

จะเกิดอะไรขึ้นหาก CSS ไม่โหลด

ภาพหน้าจอของปุ่มเบราว์เซอร์ธรรมดาที่มีไอคอนดวงอาทิตย์อยู่ด้านใน

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

เลย์เอาต์

คอมโพเนนต์สวิตช์ธีมมีพื้นที่ผิวน้อย คุณจึงไม่จำเป็นต้องใช้ตารางกริดหรือ Flexbox สำหรับเลย์เอาต์ แต่จะใช้การจัดตําแหน่ง SVG และการเปลี่ยนรูปแบบ CSS แทน

รูปแบบ

.theme-toggle รูปแบบ

องค์ประกอบ <button> คือคอนเทนเนอร์สำหรับรูปร่างและรูปแบบไอคอน บริบทหลักนี้จะเก็บสีและขนาดแบบปรับเปลี่ยนได้เพื่อส่งต่อไปยัง SVG

งานแรกคือเปลี่ยนปุ่มเป็นรูปวงกลมและนำสไตล์ปุ่มเริ่มต้นออก

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

ต่อไป ให้เพิ่มรูปแบบการโต้ตอบ เพิ่มสไตล์เคอร์เซอร์สำหรับผู้ที่ใช้เมาส์ เพิ่ม touch-action: manipulation เพื่อประสบการณ์การสัมผัสที่ตอบสนองอย่างรวดเร็ว นำไฮไลต์แบบโปร่งแสงบางส่วนที่ iOS ใช้กับปุ่มออก สุดท้าย ให้เว้นขอบขององค์ประกอบไว้บ้างสำหรับเส้นขอบของสถานะโฟกัส

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

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

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

การปรับขนาดแบบปรับเปลี่ยนได้ด้วยการค้นหาสื่อ hover

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

.theme-toggle {
  --size: 2rem;
  
  
  @media (hover: none) {
    --size: 48px;
  }
}

สไตล์ SVG ของดวงอาทิตย์และดวงจันทร์

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

ธีมสว่าง

ALT_TEXT_HERE

หากต้องการให้ภาพเคลื่อนไหวของการขยายและการหมุนเกิดขึ้นจากจุดศูนย์กลางของรูปร่าง SVG ให้ตั้งค่า transform-origin: center center รูปร่างใช้สีที่ปรับเปลี่ยนได้ซึ่งปุ่มระบุไว้ ดวงจันทร์และดวงอาทิตย์ใช้ปุ่มที่มีให้ var(--icon-fill) และ var(--icon-fill-hover) สำหรับสีเติม ส่วนแสงอาทิตย์ใช้ตัวแปรสำหรับเส้นโครงร่าง

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

ธีมมืด

ALT_TEXT_HERE

รูปแบบดวงจันทร์ต้องนำแสงแดดออก ปรับขนาดวงกลมดวงอาทิตย์ให้ใหญ่ขึ้น และย้ายมาสก์วงกลม

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
      }
    }
  }
}

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

แอนิเมชัน

ปุ่มควรใช้งานได้และมีสถานะ แต่ไม่มีการเปลี่ยนรูปแบบในขั้นตอนนี้ ส่วนต่อไปนี้จะอธิบายวิธีและสิ่งที่จะเปลี่ยน

การแชร์คำค้นหาสื่อและการนําเข้า easing

ปลั๊กอิน Custom Media ของ PostCSS ช่วยให้ใช้ไวยากรณ์ข้อกำหนด CSS ฉบับร่างสำหรับตัวแปรคำค้นหาสื่อได้ ซึ่งช่วยให้ใส่ทรานซิชันและภาพเคลื่อนไหวตามค่ากำหนดการเคลื่อนไหวของระบบปฏิบัติการของผู้ใช้ได้อย่างง่ายดาย

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

หากต้องการใช้ easing ของ CSS ที่ไม่เหมือนใครและใช้งานง่าย ให้นําเข้าส่วน easings ของ Open Props ดังนี้

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

ดวงอาทิตย์

การเปลี่ยนเฟรมของดวงอาทิตย์จะดูสนุกสนานกว่าดวงจันทร์ ซึ่งจะได้เอฟเฟกต์นี้ด้วยการใช้ easing แบบ bouncy แสงแดดควรสั่นเล็กน้อยขณะหมุน และจุดศูนย์กลางของดวงอาทิตย์ควรสั่นเล็กน้อยขณะเปลี่ยนขนาด

สไตล์เริ่มต้น (ธีมสว่าง) จะกำหนดการเปลี่ยนรูปแบบ และสไตล์ธีมมืดจะกำหนดการปรับแต่งสำหรับการเปลี่ยนเป็นธีมสว่าง

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

ในแผงภาพเคลื่อนไหวในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome คุณจะเห็นไทม์ไลน์สำหรับทรานซิชันภาพเคลื่อนไหว คุณสามารถตรวจสอบระยะเวลาของภาพเคลื่อนไหวทั้งหมด องค์ประกอบ และเวลาของ easing

การเปลี่ยนจากสว่างเป็นมืด
การเปลี่ยนจากมืดเป็นสว่าง

ดวงจันทร์

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

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

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
การเปลี่ยนจากสว่างเป็นมืด
การเปลี่ยนจากมืดเป็นสว่าง

ต้องการการเคลื่อนไหวที่ลดลง

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

JavaScript

JavaScript ทำงานหลายอย่างในคอมโพเนนต์นี้ ตั้งแต่การจัดการข้อมูล ARIA สำหรับโปรแกรมอ่านหน้าจอไปจนถึงการรับและตั้งค่าจากพื้นที่เก็บข้อมูลในเครื่อง

ประสบการณ์การโหลดหน้าเว็บ

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

<script src="./theme-toggle.js"></script>

โดยระบบจะโหลดแท็ก <script> ธรรมดาในเอกสาร <head> ก่อนมาร์กอัป CSS หรือ <body> เมื่อเบราว์เซอร์พบสคริปต์ที่ไม่มีเครื่องหมายเช่นนี้ ก็จะเรียกใช้โค้ดและดำเนินการก่อน HTML ที่เหลือ การใช้ช่วงเวลาการบล็อกนี้อย่างประหยัดจะช่วยให้คุณตั้งค่าแอตทริบิวต์ HTML ได้ก่อนที่ CSS หลักจะแสดงหน้าเว็บ จึงป้องกันไม่ให้หน้าเว็บกะพริบหรือมีสี

JavaScript จะตรวจสอบค่ากําหนดของผู้ใช้ในพื้นที่เก็บข้อมูลในเครื่องก่อน และเปลี่ยนไปตรวจสอบค่ากําหนดของระบบหากไม่พบค่าใดๆ ในพื้นที่เก็บข้อมูล

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

ระบบจะแยกวิเคราะห์ฟังก์ชันเพื่อตั้งค่าค่ากําหนดของผู้ใช้ในพื้นที่เก็บข้อมูลในเครื่องต่อ

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

ตามด้วยฟังก์ชันสำหรับแก้ไขเอกสารด้วยค่ากำหนด

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

สิ่งที่ควรทราบในตอนนี้คือสถานะการแยกวิเคราะห์เอกสาร HTML เบราว์เซอร์ยังไม่รู้จักปุ่ม "#theme-toggle" เนื่องจากยังไม่ได้แยกวิเคราะห์แท็ก <head> จนเสร็จสมบูรณ์ อย่างไรก็ตาม เบราว์เซอร์มี document.firstElementChild หรือที่เรียกว่าแท็ก <html> ฟังก์ชันจะพยายามตั้งค่าทั้ง 2 รายการเพื่อให้ซิงค์กันอยู่เสมอ แต่ในการเรียกใช้ครั้งแรกจะตั้งค่าได้เฉพาะแท็ก HTML querySelector จะยังไม่พบรายการใดๆ ในตอนแรก และโอเปอเรเตอร์การเชนทางเลือกจะป้องกันไม่ให้เกิดข้อผิดพลาดทางไวยากรณ์เมื่อไม่พบรายการดังกล่าวและพยายามเรียกใช้ฟังก์ชัน setAttribute

จากนั้นระบบจะเรียกใช้ฟังก์ชัน reflectPreference() นั้นทันทีเพื่อให้เอกสาร HTML มีการตั้งค่าแอตทริบิวต์ data-theme ดังนี้

reflectPreference()

ปุ่มยังคงต้องใช้แอตทริบิวต์ ดังนั้นโปรดรอเหตุการณ์การโหลดหน้าเว็บ จากนั้นจึงค้นหา เพิ่ม Listener และตั้งค่าแอตทริบิวต์ใน

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

ประสบการณ์การสลับปุ่ม

เมื่อคลิกปุ่ม จะต้องมีการเปลี่ยนธีมทั้งในหน่วยความจำ JavaScript และในเอกสาร จะต้องตรวจสอบค่าธีมปัจจุบันและตัดสินใจเกี่ยวกับสถานะใหม่ เมื่อตั้งค่าสถานะใหม่แล้ว ให้บันทึกและอัปเดตเอกสาร ดังนี้

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

กำลังซิงค์กับระบบ

สิ่งที่ทำให้การเปลี่ยนธีมนี้แตกต่างจากการเปลี่ยนธีมอื่นๆ คือจะมีการซิงค์กับค่ากําหนดของระบบเมื่อค่ากําหนดมีการเปลี่ยนแปลง หากผู้ใช้เปลี่ยนค่ากำหนดของระบบขณะที่หน้าเว็บและคอมโพเนนต์นี้แสดงอยู่ สวิตช์ธีมจะเปลี่ยนให้ตรงกับค่ากำหนดใหม่ของผู้ใช้ ราวกับว่าผู้ใช้โต้ตอบกับสวิตช์ธีมในเวลาเดียวกับที่ระบบเปลี่ยน

ทําได้โดยการใช้ JavaScript และ matchMedia ฟังเหตุการณ์เพื่อตรวจหาการเปลี่ยนแปลงในคําค้นหาสื่อ

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
การเปลี่ยนค่ากำหนดของระบบ MacOS จะเปลี่ยนสถานะสวิตช์ธีม

บทสรุป

ตอนนี้คุณรู้วิธีที่เราทำแล้ว คุณจะทำอย่างไรบ้าง 🙂

มาลองใช้แนวทางที่หลากหลายและดูวิธีทั้งหมดในการสร้างบนเว็บกัน สร้างเดโม แล้วทวีตลิงก์มาหาเรา เราจะเพิ่มลงในส่วนรีมิกซ์ของชุมชนด้านล่าง

รีมิกซ์ของชุมชน