การสร้างคอมโพเนนต์แท็บ

ภาพรวมพื้นฐานเกี่ยวกับวิธีสร้างคอมโพเนนต์แท็บที่คล้ายกับที่พบในแอป iOS และ Android

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

สาธิต

หากคุณชอบดูวิดีโอมากกว่า เรามีโพสต์นี้ในเวอร์ชัน YouTube ด้วย

ภาพรวม

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

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

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

กลยุทธ์สำหรับเว็บ

โดยรวมแล้ว ฉันพบว่าคอมโพเนนต์นี้สร้างได้ค่อนข้างตรงไปตรงมา เนื่องจากมีฟีเจอร์สำคัญบางอย่างของแพลตฟอร์มเว็บ ดังนี้

  • scroll-snap-points สำหรับการโต้ตอบด้วยการปัดและแป้นพิมพ์ที่สวยงามพร้อมตำแหน่งหยุดการเลื่อนที่เหมาะสม
  • Deep Link ผ่านแฮช URL สำหรับการรองรับการแชร์และการตรึงการเลื่อนในหน้าเว็บที่จัดการโดยเบราว์เซอร์
  • การรองรับโปรแกรมอ่านหน้าจอด้วยมาร์กอัปองค์ประกอบ <a> และ id="#hash"
  • prefers-reduced-motion สำหรับการเปิดใช้การเปลี่ยนแบบครอสเฟดและการเลื่อนในหน้าเว็บแบบทันที
  • ฟีเจอร์เว็บ @scroll-timeline ฉบับร่างสำหรับการขีดเส้นใต้แบบไดนามิกและ การเปลี่ยนสีแท็บที่เลือก

HTML

โดยพื้นฐานแล้ว UX ในที่นี้คือคลิกลิงก์ ให้ URL แสดงสถานะหน้าเว็บที่ฝังอยู่ แล้วดูการอัปเดตพื้นที่เนื้อหาขณะที่เบราว์เซอร์เลื่อนไปยังองค์ประกอบที่ตรงกัน

มีสมาชิกเนื้อหาเชิงโครงสร้างบางส่วนอยู่ในนั้น ได้แก่ ลิงก์และ :target เราต้องการรายการลิงก์ ซึ่ง <nav> เหมาะมาก และรายการองค์ประกอบ <article> ซึ่ง <section> เหมาะมาก แฮชลิงก์แต่ละรายการจะตรงกับส่วนหนึ่งๆ ซึ่งช่วยให้เบราว์เซอร์เลื่อนสิ่งต่างๆ ผ่านการยึดตำแหน่งได้

เมื่อคลิกปุ่มลิงก์ เนื้อหาที่โฟกัสจะเลื่อนเข้ามา

เช่น การคลิกลิงก์จะโฟกัสบทความ :target ใน Chrome 89 โดยอัตโนมัติ โดยไม่ต้องใช้ JS จากนั้นผู้ใช้จะเลื่อนดูเนื้อหาบทความด้วยอุปกรณ์อินพุตได้ตามปกติ เป็นเนื้อหาเสริมตามที่ระบุไว้ในมาร์กอัป

ฉันใช้มาร์กอัปต่อไปนี้เพื่อจัดระเบียบแท็บ

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

ฉันสามารถสร้างการเชื่อมต่อระหว่างองค์ประกอบ <a> และ <article> ด้วยพร็อพเพอร์ตี้ href และ id ได้ดังนี้

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

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

เลย์เอาต์แบบเลื่อน

พื้นที่เลื่อนในคอมโพเนนต์นี้มี 3 ประเภท ได้แก่

  • การนำทาง (สีชมพู) เลื่อนได้ในแนวนอน
  • พื้นที่เนื้อหา (สีน้ำเงิน) เลื่อนได้ในแนวนอน
  • รายการบทความแต่ละรายการ (สีเขียว) จะเลื่อนในแนวตั้งได้
กล่องสีสันสดใส 3 กล่องที่มีลูกศรบอกทิศทางซึ่งมีสีตรงกัน ซึ่งแสดงโครงร่างของพื้นที่เลื่อนและแสดงทิศทางที่จะเลื่อน

องค์ประกอบที่เกี่ยวข้องกับการเลื่อนมี 2 ประเภท ดังนี้

  1. หน้าต่าง
    กล่องที่มีขนาดที่กำหนดซึ่งมีรูปแบบพร็อพเพอร์ตี้ overflow
  2. พื้นผิวขนาดใหญ่เกินไป
    ในเลย์เอาต์นี้ พื้นผิวขนาดใหญ่เกินไปคือคอนเทนเนอร์รายการ ซึ่งได้แก่ ลิงก์การนำทาง บทความในส่วน และเนื้อหาบทความ

เลย์เอาต์ <snap-tabs>

เลย์เอาต์ระดับบนสุดที่ฉันเลือกคือ Flex (Flexbox) ฉันตั้งค่าทิศทางเป็น column ดังนั้นส่วนหัวและส่วนต่างๆ จึงจัดเรียงตามแนวตั้ง นี่คือหน้าต่างเลื่อนแรกของเรา และจะซ่อนทุกอย่างด้วย overflow hidden ส่วนหัวและส่วนจะใช้การเลื่อนเกินขอบเขตในเร็วๆ นี้เป็นโซนแยกกัน

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
  snap-tabs {
  display: flex;
  flex-direction: column;

  /* establish primary containing box */
  overflow: hidden;
  position: relative;

  & > section {
    /* be pushy about consuming all space */
    block-size: 100%;
  }

  & > header {
    /* defend against 
needing 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }

ชี้กลับไปที่แผนภาพ 3 ม้วนที่มีสีสัน

  • ตอนนี้ <header> พร้อมที่จะเป็นคอนเทนเนอร์การเลื่อน (สีชมพู) แล้ว
  • <section> เตรียมพร้อมที่จะเป็นคอนเทนเนอร์การเลื่อน(สีน้ำเงิน)

เฟรมที่ฉันไฮไลต์ไว้ด้านล่างด้วย VisBug ช่วยให้เราเห็นหน้าต่างที่คอนเทนเนอร์การเลื่อนสร้างขึ้น

องค์ประกอบส่วนหัวและส่วนมีภาพซ้อนทับสีชมพูสด ซึ่งแสดงพื้นที่ที่องค์ประกอบใช้ในคอมโพเนนต์

เลย์เอาต์แท็บ <header>

เลย์เอาต์ถัดไปเกือบจะเหมือนกัน โดยฉันใช้ Flex เพื่อสร้างการจัดเรียงแนวตั้ง

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

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

องค์ประกอบ nav และ span.indicator มีการวางซ้อนสีชมพูสด ซึ่งแสดงโครงร่างของพื้นที่ที่องค์ประกอบใช้ในคอมโพเนนต์

ถัดไปคือรูปแบบการเลื่อน ปรากฏว่าเราสามารถแชร์รูปแบบการเลื่อนระหว่างพื้นที่เลื่อนแนวนอน 2 ส่วน (ส่วนหัวและส่วน) ได้ ดังนั้นฉันจึงสร้างคลาสยูทิลิตี .scroll-snap-x ขึ้นมา

.scroll-snap-x {
  /* browser decide if x is ok to scroll and show bars on, y hidden */
  overflow: auto hidden;
  /* prevent scroll chaining on x scroll */
  overscroll-behavior-x: contain;
  /* scrolling should snap children on x */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

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

เลย์เอาต์ส่วนหัวของแท็บ <nav>

ลิงก์การนำทางต้องจัดวางเป็นบรรทัดเดียวโดยไม่มีการขึ้นบรรทัดใหม่ จัดกึ่งกลางในแนวตั้ง และรายการลิงก์แต่ละรายการควรจัดพอดีกับการสแนปการเลื่อนของคอนเทนเนอร์ Swift work for 2021 CSS!

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
  nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

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

องค์ประกอบ a ของ nav มีการวางซ้อนสีชมพูสด ซึ่งแสดงโครงร่างของพื้นที่ที่องค์ประกอบใช้ในคอมโพเนนต์ รวมถึงตำแหน่งที่องค์ประกอบล้นออกมา

เลย์เอาต์แท็บ <section>

This section is a flex item and needs to be the dominant consumer of space. นอกจากนี้ยังต้องสร้างคอลัมน์สำหรับวางบทความด้วย ขอขอบคุณอีกครั้งสำหรับการทำงานอย่างรวดเร็วใน CSS 2021 block-size: 100% จะขยายองค์ประกอบนี้ให้เต็มองค์ประกอบหลักมากที่สุด จากนั้นสำหรับเลย์เอาต์ของตัวเอง ระบบจะสร้างชุดคอลัมน์ที่มีความกว้าง 100% ขององค์ประกอบหลัก เปอร์เซ็นต์ใช้ได้ดีในกรณีนี้เพราะเราได้เขียนข้อจำกัดที่เข้มงวดในระดับบนสุด

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
  section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

ซึ่งเหมือนกับว่าเรากำลังพูดว่า "ขยายในแนวตั้งให้มากที่สุดเท่าที่จะเป็นไปได้ในลักษณะที่ผลักดัน" (โปรดจำส่วนหัวที่เราตั้งค่าเป็น flex-shrink: 0 ซึ่งเป็นการป้องกันการขยายนี้) ซึ่งจะกำหนดความสูงของแถวสำหรับชุดคอลัมน์ที่มีความสูงเต็ม สไตล์ auto-flow จะบอกให้ตารางกริดจัดวางองค์ประกอบย่อยในแนวนอนเสมอ ไม่มีการตัดขึ้นบรรทัดใหม่ ซึ่งตรงกับสิ่งที่เราต้องการ นั่นคือให้ล้นหน้าต่างหลัก

องค์ประกอบบทความมีการวางซ้อนสีชมพูสด ซึ่งแสดงโครงร่างของพื้นที่ที่องค์ประกอบใช้ในคอมโพเนนต์และตำแหน่งที่ล้นออกมา

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

เลย์เอาต์แท็บ <article>

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

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

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

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

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

สรุปพื้นที่เลื่อน 3 รายการ

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

แถบเลื่อนทั้ง 3 แถบได้รับการตั้งค่าให้แสดง โดยตอนนี้ใช้พื้นที่เลย์เอาต์ และคอมโพเนนต์ของเรายังคงดูดี

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

เครื่องมือสำหรับนักพัฒนาเว็บช่วยให้เราเห็นภาพนี้ได้

พื้นที่เลื่อนมีเครื่องมือวางซ้อนแบบตารางกริดและ Flexbox ซึ่งแสดงโครงร่างของพื้นที่ที่ใช้ในคอมโพเนนต์และทิศทางที่ล้นออกมา
เครื่องมือสำหรับนักพัฒนาเว็บใน Chromium แสดงเลย์เอาต์องค์ประกอบการนำทาง Flexbox ที่เต็มไปด้วยองค์ประกอบ Anchor, เลย์เอาต์ส่วนตารางกริดที่เต็มไปด้วยองค์ประกอบบทความ และองค์ประกอบบทความที่เต็มไปด้วยย่อหน้าและองค์ประกอบส่วนหัว

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

ไฮไลต์ฟีเจอร์

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

แอนิเมชัน

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

ฉันจะลิงก์ขีดเส้นใต้แท็บกับตำแหน่งการเลื่อนของบทความ การสแนปไม่ได้เป็นเพียงการจัดแนวที่สวยงามเท่านั้น แต่ยังเป็นการยึดจุดเริ่มต้นและจุดสิ้นสุดของภาพเคลื่อนไหวด้วย วิธีนี้จะช่วยให้ <nav> ซึ่งทำหน้าที่เหมือนแผนที่ย่อเชื่อมโยงกับเนื้อหา เราจะตรวจสอบค่ากำหนดการเคลื่อนไหวของผู้ใช้จากทั้ง CSS และ JS มีสถานที่ที่เหมาะแก่การแสดงความเห็นอกเห็นใจอยู่ 2-3 แห่ง

ลักษณะการเลื่อน

มีโอกาสที่จะปรับปรุงลักษณะการเคลื่อนไหวของทั้ง :target และ element.scrollIntoView() โดยค่าเริ่มต้น ระบบจะดำเนินการทันที เบราว์เซอร์จะตั้งค่าตำแหน่งการเลื่อนเท่านั้น แล้วถ้าเราต้องการเปลี่ยนไปยังตำแหน่งการเลื่อนนั้นแทนที่จะกะพริบที่นั่นล่ะ

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

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

สัญญาณบอกสถานะแท็บ

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

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

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

ฉันซ่อน .snap-indicator เมื่อผู้ใช้ต้องการลดการเคลื่อนไหวเนื่องจากไม่จำเป็นต้องใช้แล้ว จากนั้นฉันจะแทนที่ด้วยสไตล์ border-block-end และ transition นอกจากนี้ โปรดสังเกตในการโต้ตอบของแท็บว่ารายการการนำทางที่ใช้งานอยู่ไม่เพียงแต่มีไฮไลต์ขีดเส้นใต้ของแบรนด์เท่านั้น แต่สีข้อความยังเข้มขึ้นด้วย องค์ประกอบที่ใช้งานอยู่จะมีคอนทราสต์ของสีข้อความที่สูงขึ้นและมีแสงเน้นสีสว่างด้านล่าง

CSS เพิ่มเติมเพียงไม่กี่บรรทัดจะทำให้ผู้ใช้รู้สึกว่าได้รับการใส่ใจ (ในแง่ที่เราคำนึงถึงค่ากำหนดการเคลื่อนไหวของผู้ใช้อย่างรอบคอบ) ฉันชอบมากเลย

@scroll-timeline

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

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

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

if (motionOK) {
  // motion based animation code
}

ในขณะที่เขียนบทความนี้ เบราว์เซอร์ยังไม่รองรับ @scroll-timeline ซึ่งเป็นข้อกำหนดฉบับร่างที่มีเพียงการใช้งานเชิงทดลองเท่านั้น แต่ก็มี Polyfill ซึ่งฉันใช้ในการสาธิตนี้

ScrollTimeline

แม้ว่าทั้ง CSS และ JavaScript จะสร้างไทม์ไลน์การเลื่อนได้ แต่ฉันเลือกใช้ JavaScript เพื่อให้ใช้การวัดองค์ประกอบแบบเรียลไทม์ในภาพเคลื่อนไหวได้

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // scroll in the direction letters flow
  fill: 'both',              // bi-directional linking
});

ฉันต้องการให้สิ่งหนึ่งติดตามตำแหน่งการเลื่อนของอีกสิ่งหนึ่ง และการสร้าง ScrollTimeline จะเป็นการกำหนดตัวขับเคลื่อนของลิงก์การเลื่อน ซึ่งก็คือ scrollSource โดยปกติแล้ว ภาพเคลื่อนไหวบนเว็บจะทำงานตามช่วงเวลาทั่วโลก แต่ด้วย sectionScrollTimeline ที่กำหนดเองในหน่วยความจำ ฉันสามารถเปลี่ยนแปลงทั้งหมดนั้นได้

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

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

คีย์เฟรมแบบไดนามิก

มีวิธี CSS แบบประกาศล้วนๆ ที่มีประสิทธิภาพมากในการสร้างภาพเคลื่อนไหวด้วย @scroll-timeline แต่ภาพเคลื่อนไหวที่ฉันเลือกทำนั้นเป็นแบบไดนามิกเกินไป ไม่มีวิธีเปลี่ยนระหว่างความกว้าง auto และไม่มีวิธีสร้างคีย์เฟรมจำนวนหนึ่งแบบไดนามิกตามความยาวขององค์ประกอบย่อย

แต่ JavaScript รู้วิธีรับข้อมูลดังกล่าว ดังนั้นเราจะทำซ้ำกับองค์ประกอบย่อยด้วยตนเองและรับค่าที่คำนวณได้ที่รันไทม์

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

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

ตัวอย่างเอาต์พุตตามค่ากำหนดแบบอักษรและเบราว์เซอร์ของฉันมีดังนี้

คีย์เฟรม TranslateX:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

คีย์เฟรมความกว้าง:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]

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

แท็บที่ใช้งานอยู่และแท็บที่ไม่ได้ใช้งานจะแสดงพร้อมการวางซ้อน VisBug ซึ่งแสดงคะแนนคอนทราสต์ที่ผ่านเกณฑ์สำหรับทั้ง 2 แท็บ

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

คุณอาจไม่ทันสังเกต แต่ฉันภูมิใจมากกับการเปลี่ยนสีเมื่อเลือกรายการการนำทางที่ไฮไลต์

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

โดยฉันทำดังนี้

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

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

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// results in 4 array items, which represent 4 keyframe states
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

คีย์เฟรมที่มีสี var(--text-active-color) จะไฮไลต์ลิงก์ และจะเป็นสีข้อความมาตรฐานในกรณีอื่นๆ ลูปที่ซ้อนกันทำให้ค่อนข้างตรงไปตรงมา เนื่องจากลูปด้านนอกคือรายการการนำทางแต่ละรายการ และลูปด้านในคือคีย์เฟรมส่วนตัวของรายการการนำทางแต่ละรายการ ฉันจะตรวจสอบว่าองค์ประกอบลูปด้านนอกเหมือนกับองค์ประกอบลูปด้านในหรือไม่ และใช้ข้อมูลนั้นเพื่อดูว่ามีการเลือกองค์ประกอบเมื่อใด

ฉันสนุกมากที่ได้เขียนเรื่องนี้ มาก

การเพิ่มประสิทธิภาพ JavaScript เพิ่มเติม

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

Deep Link เป็นคำที่ใช้กับอุปกรณ์เคลื่อนที่มากกว่า แต่เราคิดว่าเจตนาของ Deep Link นั้นตรงกับแท็บตรงที่คุณสามารถแชร์ URL ไปยังเนื้อหาของแท็บได้โดยตรง เบราว์เซอร์จะนำทางในหน้าไปยังรหัสที่ตรงกันในแฮช URL ฉันพบว่าผู้จัดการ onload รายนี้สร้างเอฟเฟกต์ในแพลตฟอร์มต่างๆ

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

การซิงค์การสิ้นสุดการเลื่อน

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

วิธีรอให้การเลื่อนสิ้นสุดมีดังนี้ js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });

เมื่อใดก็ตามที่มีการเลื่อนส่วนต่างๆ ให้ล้างการหมดเวลาของส่วน (หากมี) และเริ่มการหมดเวลาใหม่ เมื่อหยุดเลื่อนส่วนต่างๆ อย่าล้างการหมดเวลา และเริ่มทำงาน 100 มิลลิวินาทีหลังจากหยุดพัก เมื่อเหตุการณ์เริ่มทำงาน ให้เรียกใช้ฟังก์ชันที่พยายามค้นหาว่าผู้ใช้หยุดดูที่จุดใด

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

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

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

การตั้งค่าแท็บที่ใช้งานอยู่จะเริ่มด้วยการล้างแท็บที่ใช้งานอยู่ในปัจจุบัน จากนั้นจึงกำหนดแอตทริบิวต์สถานะที่ใช้งานอยู่ให้กับรายการการนำทางที่เข้ามา การเรียกใช้ scrollIntoView() มีการโต้ตอบที่สนุกสนานกับ CSS ซึ่งน่าสนใจ

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

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

บทสรุป

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

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

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