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

ภาพรวมพื้นฐานของวิธีสร้างคอมโพเนนต์แท็บที่คล้ายกับที่พบในแอป 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. พื้นผิวที่มีขนาดใหญ่เกินไป
    ในเลย์เอาต์นี้คือคอนเทนเนอร์รายการ ได้แก่ nav links, section articles และ article contents

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

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

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

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

ลิงก์นำทางต้องจัดวางเป็นบรรทัดเดียวโดยไม่มีการแบ่งบรรทัด จัดกึ่งกลางในแนวตั้ง และรายการลิงก์แต่ละรายการควรสแนปกับคอนเทนเนอร์ Scroll-Snap 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>

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

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

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

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

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

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

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

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

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

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

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

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

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

แอนิเมชัน

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

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

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

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

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

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

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 แล้วทวีตหาฉัน พร้อมเวอร์ชันของคุณ แล้วฉันจะเพิ่มลงในส่วนรีมิกซ์ของชุมชน ด้านล่าง

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