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

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

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

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

หากชอบวิดีโอ นี่คือโพสต์นี้เวอร์ชัน YouTube

ภาพรวม

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

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

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

Markup

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

ปุ่ม

ปุ่มต้องมีคลาสเพื่อการใช้งานจาก CSS และรหัสสำหรับใช้งานจาก JavaScript นอกจากนี้ เนื่องจากเนื้อหาของปุ่มจะเป็นไอคอน ไม่ใช่ข้อความ ให้เพิ่มแอตทริบิวต์ title [ชื่อ] เพื่อระบุข้อมูลเกี่ยวกับวัตถุประสงค์ของปุ่ม สุดท้าย ให้เพิ่ม [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 ชั้นเพื่อช่วยแสดงวิธีการทำงานของการมาสก์ เลเยอร์ด้านบนเป็นสี่เหลี่ยมจัตุรัสสีขาวที่มีวงกลมสีดำ ส่วนชั้นกลางคือไอคอนดวงอาทิตย์
เลเยอร์ด้านล่างจะมีป้ายกำกับเป็นผลลัพธ์และแสดงไอคอนรูปดวงอาทิตย์ที่มีคัตเอาต์ที่วงกลมสีดำของเลเยอร์ด้านบนอยู่

มาสก์ที่มี 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: 1) {
        transform: translateX(0);
        cx: 17;
      }
    }
  }
}

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

แอนิเมชัน

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

การแชร์คำค้นหาสื่อและการนำเข้าการค่อยๆ เปลี่ยน

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

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

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

เพื่อให้การค่อยๆ เปลี่ยน CSS ใช้งานง่ายและไม่เหมือนใคร ให้นำเข้าส่วนการค่อยๆ เปลี่ยนของ Open Props ดังนี้

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

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

ดวงอาทิตย์

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

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

​​.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 คุณจะเห็นไทม์ไลน์สำหรับการเปลี่ยนภาพเคลื่อนไหว สามารถตรวจสอบระยะเวลาของภาพเคลื่อนไหวทั้งหมด องค์ประกอบ และเวลาการค่อยๆ เปลี่ยนได้

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

ดวงจันทร์

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

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

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

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
        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 จะเปลี่ยนสถานะการเปลี่ยนธีม

บทสรุป

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

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

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