ภาพรวมพื้นฐานเกี่ยวกับวิธีสร้างคอมโพเนนต์แท็บที่คล้ายกับที่พบในแอป iOS และ Android
ในโพสต์นี้ เราอยากแชร์แนวคิดเกี่ยวกับการสร้างคอมโพเนนต์แท็บสำหรับเว็บที่ปรับเปลี่ยนตามอุปกรณ์ รองรับอินพุตจากอุปกรณ์หลายเครื่อง และทำงานได้ในเบราว์เซอร์ต่างๆ ลองใช้เดโม
หากคุณชอบดูวิดีโอมากกว่า เรามีโพสต์นี้ในเวอร์ชัน YouTube ด้วย
ภาพรวม
แท็บเป็นองค์ประกอบทั่วไปของระบบการออกแบบ แต่อาจมีรูปร่างและรูปแบบที่หลากหลาย เริ่มแรกมีแท็บเดสก์ท็อปที่สร้างขึ้นจากองค์ประกอบ <frame> และตอนนี้เรามีคอมโพเนนต์บนอุปกรณ์เคลื่อนที่ที่ลื่นไหลซึ่งแสดงภาพเคลื่อนไหวของเนื้อหาตามคุณสมบัติทางกายภาพ
ทั้งหมดนี้พยายามทำสิ่งเดียวกัน นั่นคือการประหยัดพื้นที่
ปัจจุบัน สิ่งสำคัญของประสบการณ์ของผู้ใช้แท็บคือพื้นที่การนำทางของปุ่มซึ่งสลับระดับการเข้าถึงเนื้อหาในเฟรมการแสดงผล พื้นที่เนื้อหาที่แตกต่างกันหลายแห่งใช้พื้นที่เดียวกัน แต่จะแสดงตามเงื่อนไขโดยอิงตามปุ่มที่เลือกในการนำทาง
กลยุทธ์สำหรับเว็บ
โดยรวมแล้ว ฉันพบว่าคอมโพเนนต์นี้สร้างได้ค่อนข้างตรงไปตรงมา เนื่องจากมีฟีเจอร์สำคัญบางอย่างของแพลตฟอร์มเว็บ ดังนี้
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 ประเภท ได้แก่
- การนำทาง (สีชมพู) เลื่อนได้ในแนวนอน
- พื้นที่เนื้อหา (สีน้ำเงิน) เลื่อนได้ในแนวนอน
- รายการบทความแต่ละรายการ (สีเขียว) จะเลื่อนในแนวตั้งได้
องค์ประกอบที่เกี่ยวข้องกับการเลื่อนมี 2 ประเภท ดังนี้
- หน้าต่าง
กล่องที่มีขนาดที่กำหนดซึ่งมีรูปแบบพร็อพเพอร์ตี้overflow - พื้นผิวขนาดใหญ่เกินไป
ในเลย์เอาต์นี้ พื้นผิวขนาดใหญ่เกินไปคือคอนเทนเนอร์รายการ ซึ่งได้แก่ ลิงก์การนำทาง บทความในส่วน และเนื้อหาบทความ
เลย์เอาต์ <snap-tabs>
เลย์เอาต์ระดับบนสุดที่ฉันเลือกคือ Flex (Flexbox) ฉันตั้งค่าทิศทางเป็น column ดังนั้นส่วนหัวและส่วนต่างๆ จึงจัดเรียงตามแนวตั้ง นี่คือหน้าต่างเลื่อนแรกของเรา และจะซ่อนทุกอย่างด้วย overflow hidden ส่วนหัวและส่วนจะใช้การเลื่อนเกินขอบเขตในเร็วๆ นี้เป็นโซนแยกกัน
<snap-tabs> <header></header> <section></section> </snap-tabs>
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 againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
ชี้กลับไปที่แผนภาพ 3 ม้วนที่มีสีสัน
- ตอนนี้
<header>พร้อมที่จะเป็นคอนเทนเนอร์การเลื่อน (สีชมพู) แล้ว <section>เตรียมพร้อมที่จะเป็นคอนเทนเนอร์การเลื่อน(สีน้ำเงิน)
เฟรมที่ฉันไฮไลต์ไว้ด้านล่างด้วย VisBug ช่วยให้เราเห็นหน้าต่างที่คอนเทนเนอร์การเลื่อนสร้างขึ้น
เลย์เอาต์แท็บ <header>
เลย์เอาต์ถัดไปเกือบจะเหมือนกัน โดยฉันใช้ Flex เพื่อสร้างการจัดเรียงแนวตั้ง
<snap-tabs>
<header>
<nav></nav>
<span class="snap-indicator"></span>
</header>
<section></section>
</snap-tabs>header { display: flex; flex-direction: column; }
.snap-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!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
ลิงก์แต่ละรายการจะจัดรูปแบบและขนาดของตัวเอง ดังนั้นเลย์เอาต์การนำทางจึงต้องระบุเฉพาะทิศทางและขั้นตอนเท่านั้น ความกว้างที่ไม่ซ้ำกันของรายการการนำทางทำให้การเปลี่ยนระหว่างแท็บเป็นเรื่องสนุก เนื่องจากตัวบ่งชี้จะปรับความกว้างให้เข้ากับเป้าหมายใหม่ เบราว์เซอร์จะแสดงแถบเลื่อนหรือไม่นั้นขึ้นอยู่กับจำนวนองค์ประกอบที่อยู่ในนี้
เลย์เอาต์แท็บ <section>
This section is a flex item and needs to be the dominant consumer of space. นอกจากนี้ยังต้องสร้างคอลัมน์สำหรับวางบทความด้วย ขอขอบคุณอีกครั้งสำหรับการทำงานอย่างรวดเร็วใน CSS 2021 block-size: 100% จะขยายองค์ประกอบนี้ให้เต็มองค์ประกอบหลักมากที่สุด จากนั้นสำหรับเลย์เอาต์ของตัวเอง ระบบจะสร้างชุดคอลัมน์ที่มีความกว้าง 100% ขององค์ประกอบหลัก เปอร์เซ็นต์ใช้ได้ดีในกรณีนี้เพราะเราได้เขียนข้อจำกัดที่เข้มงวดในระดับบนสุด
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
ซึ่งเหมือนกับว่าเรากำลังพูดว่า "ขยายในแนวตั้งให้มากที่สุดเท่าที่จะเป็นไปได้ในลักษณะที่ผลักดัน" (โปรดจำส่วนหัวที่เราตั้งค่าเป็น flex-shrink: 0 ซึ่งเป็นการป้องกันการขยายนี้) ซึ่งจะกำหนดความสูงของแถวสำหรับชุดคอลัมน์ที่มีความสูงเต็ม สไตล์ auto-flow จะบอกให้ตารางกริดจัดวางองค์ประกอบย่อยในแนวนอนเสมอ ไม่มีการตัดขึ้นบรรทัดใหม่ ซึ่งตรงกับสิ่งที่เราต้องการ นั่นคือให้ล้นหน้าต่างหลัก
บางครั้งฉันก็รู้สึกว่าเรื่องนี้เข้าใจยาก องค์ประกอบส่วนนี้จะพอดีกับกล่อง แต่ก็สร้างชุดกล่องขึ้นมาด้วย หวังว่าภาพและคำอธิบาย จะช่วยให้เข้าใจได้ง่ายขึ้นนะ
เลย์เอาต์แท็บ <article>
ผู้ใช้ควรเลื่อนดูเนื้อหาบทความได้ และแถบเลื่อนควรปรากฏขึ้นเมื่อมีเนื้อหาล้นเท่านั้น องค์ประกอบของบทความเหล่านี้อยู่ในตำแหน่งที่เรียบร้อย โดยเป็นทั้งองค์ประกอบหลักและองค์ประกอบย่อยของการเลื่อนพร้อมกัน เบราว์เซอร์จะจัดการการโต้ตอบด้วยการสัมผัส เมาส์ และแป้นพิมพ์ที่ซับซ้อนให้เรา
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
ฉันเลือกให้บทความจัดชิดภายในตัวเลื่อนหลัก ฉันชอบที่รายการลิงก์การนำทางและองค์ประกอบบทความจัดชิดกับจุดเริ่มต้นในบรรทัดของคอนเทนเนอร์การเลื่อนที่เกี่ยวข้อง ดูเหมือนว่าทั้ง 2 ฝ่ายจะมีความสัมพันธ์ที่กลมกลืนกัน
บทความนี้เป็นองค์ประกอบย่อยของตารางกริด และขนาดของบทความจะกำหนดไว้ล่วงหน้าให้เป็นพื้นที่วิวพอร์ตที่เราต้องการมอบประสบการณ์การใช้งานการเลื่อน ซึ่งหมายความว่าฉันไม่จำเป็นต้องใช้รูปแบบความสูงหรือความกว้างที่นี่ เพียงแค่ต้องกำหนดวิธีที่เนื้อหาล้นออกมา ฉันตั้งค่า overflow-y เป็น auto แล้วดักจับการโต้ตอบการเลื่อนด้วยพร็อพเพอร์ตี้ overscroll-behavior ที่มีประโยชน์
สรุปพื้นที่เลื่อน 3 รายการ
ด้านล่างนี้คือตัวอย่างที่ฉันเลือก "แสดงแถบเลื่อนเสมอ" ในการตั้งค่าระบบ เราคิดว่าการที่เลย์เอาต์ทำงานได้เมื่อเปิดการตั้งค่านี้เป็นเรื่องสำคัญอย่างยิ่ง เช่นเดียวกับการตรวจสอบเลย์เอาต์และการจัดเรียงการเลื่อน
เราคิดว่าการเห็นรางแถบเลื่อนในคอมโพเนนต์นี้ช่วยแสดงให้เห็นอย่างชัดเจนว่าพื้นที่เลื่อนอยู่ตรงไหน ทิศทางที่รองรับ และวิธีที่พื้นที่เหล่านั้นโต้ตอบกัน พิจารณาว่าเฟรมหน้าต่างเลื่อนแต่ละเฟรมเหล่านี้เป็น Flex หรือ Grid Parent ของเลย์เอาต์ด้วย
เครื่องมือสำหรับนักพัฒนาเว็บช่วยให้เราเห็นภาพนี้ได้
เลย์เอาต์การเลื่อนเสร็จสมบูรณ์แล้ว ซึ่งรวมถึงการสแนป การลิงก์ในรายละเอียด และการเข้าถึงด้วยแป้นพิมพ์ รากฐานที่แข็งแกร่งสำหรับการปรับปรุง 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 รายการโดยขึ้นอยู่กับตำแหน่งการสแนปการเลื่อนของแถบเลื่อนส่วน จุดสแนปจะสร้างการแบ่งเขตที่ชัดเจนระหว่างคีย์เฟรมและช่วยเพิ่มความรู้สึกที่สอดคล้องกันของภาพเคลื่อนไหว
ผู้ใช้จะขับเคลื่อนภาพเคลื่อนไหวด้วยการโต้ตอบ โดยจะเห็นความกว้างและตำแหน่งของตัวบ่งชี้เปลี่ยนจากส่วนหนึ่งไปยังส่วนถัดไป ซึ่งจะติดตามการเลื่อนได้อย่างสมบูรณ์แบบ
คุณอาจไม่ทันสังเกต แต่ฉันภูมิใจมากกับการเปลี่ยนสีเมื่อเลือกรายการการนำทางที่ไฮไลต์
สีเทาอ่อนที่ไม่ได้เลือกจะดูเหมือนถูกผลักไปด้านหลังมากขึ้นเมื่อรายการที่ไฮไลต์มีคอนทราสต์มากกว่า การเปลี่ยนสีข้อความ เช่น เมื่อวางเมาส์เหนือและเมื่อเลือก เป็นเรื่องปกติ แต่การเปลี่ยนสีเมื่อเลื่อนซึ่งซิงค์กับตัวบ่งชี้ขีดเส้นใต้เป็นอีกระดับหนึ่ง
โดยฉันทำดังนี้
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 เป็นคำที่ใช้กับอุปกรณ์เคลื่อนที่มากกว่า แต่เราคิดว่าเจตนาของ 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, ทวีตเวอร์ชันของคุณมาให้ฉัน แล้วฉันจะเพิ่มลงในส่วนรีมิกซ์ของชุมชนด้านล่าง
รีมิกซ์ของชุมชน
- @devnook, @rob_dodson และ @DasSurma กับคอมโพเนนต์เว็บ: บทความ
- @jhvanderschee พร้อมปุ่ม: Codepen