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

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

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

การสาธิต

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

ภาพรวม

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

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

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

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

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

  • 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>

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

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

คอมโพเนนต์นี้มีพื้นที่สำหรับเลื่อน 3 ประเภทดังนี้

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

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

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

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

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

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 ช่วยให้เราเห็นหน้าต่าง ที่คอนเทนเนอร์การเลื่อนสร้างขึ้น

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

เลย์เอาต์ของแท็บ <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 มีการซ้อนทับ Hotpink บนนั้น ซึ่งจะระบุพื้นที่ว่างที่เกิดขึ้นในคอมโพเนนต์

ต่อไปคือรูปแบบการเลื่อน ปรากฏว่าเราสามารถแชร์รูปแบบการเลื่อนระหว่างพื้นที่เลื่อนแนวนอน 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>

ลิงก์การนำทางจะต้องวางเรียงต่อกันโดยไม่มีการแบ่งบรรทัด จัดวางในแนวตั้ง และรายการลิงก์แต่ละรายการควรสแนปไปยังคอนเทนเนอร์ของ Scroll Snap เร็วสุดๆ สำหรับ CSS ในปี 2021

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;
  }
}

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

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

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

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

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

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

เลย์เอาต์ของแท็บ <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;
}

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

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

บทความจะเป็นตารางกริดย่อยและมีการกำหนดขนาดไว้ล่วงหน้าให้เป็นพื้นที่วิวพอร์ตที่เราต้องการให้ UX ในการเลื่อน หมายความว่าผมไม่ต้องการรูปแบบความสูงหรือความกว้างที่นี่ ผมแค่ต้องกำหนดว่าจะให้แตกต่างกันอย่างไร ผมตั้งค่า overflow-y ให้เป็น "อัตโนมัติ" แล้วดักจับการโต้ตอบการเลื่อนด้วยพร็อพเพอร์ตี้ลักษณะการทำงานของการเลื่อนมากเกินไป

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

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

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

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

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

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

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

ฟีเจอร์เด่น

การเลื่อนของบุตรหลานที่สแนปไว้จะยังคงอยู่ในตำแหน่งที่ล็อกไว้ระหว่างการปรับขนาด ซึ่งหมายความว่า JavaScript ไม่จำเป็นต้องนำสิ่งใดมาแสดงเมื่อหมุนอุปกรณ์หรือปรับขนาดเบราว์เซอร์ ลองใช้ได้ในโหมดอุปกรณ์ของ Chromium DevTools โดยเลือกโหมดใดก็ได้ที่ไม่ใช่ปรับเปลี่ยนตามอุปกรณ์ จากนั้นปรับขนาดเฟรมของอุปกรณ์ องค์ประกอบจะคงอยู่ในมุมมองและล็อกไว้กับเนื้อหา ฟีเจอร์นี้พร้อมให้ใช้งานแล้วตั้งแต่ 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 เพียง 2-3 บรรทัดจะทำให้ผู้อื่นรู้สึกว่าคุณเห็น (ตามที่เราตั้งใจทำตามค่ากำหนดการเคลื่อนไหว) ฉันชอบมาก

@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
});

ฉันต้องการให้ 1 อย่างเป็นไปตามตำแหน่งการเลื่อนของอีกรายการ และการสร้าง 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 ซึ่งแสดงคะแนนคอนทราสต์ที่ผ่านสำหรับทั้ง

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

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

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

นี่คือวิธีการที่ฉันทำ:

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) จะไฮไลต์ลิงก์หรือเป็นสีข้อความมาตรฐาน ลูปที่ฝังอยู่นี้ทำให้โครงสร้างค่อนข้างตรงไปตรงมา เนื่องจากลูปด้านนอกคือรายการการนำทางแต่ละรายการ และลูปภายในคือคีย์เฟรมส่วนตัวของ Navitem แต่ละรายการ ผมตรวจสอบว่าองค์ประกอบลูปภายนอกเหมือนกัน กับลูปภายในไหม และใช้เพื่อที่จะรู้ว่าเมื่อเลือกองค์ประกอบนั้นแล้ว

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

การปรับปรุง 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 ได้แบบประกาศ เป็นการจับคู่ที่ตรงกันบ้างในบางครั้ง

บทสรุป

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

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

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