ภาพรวมพื้นฐานของวิธีสร้างองค์ประกอบแท็บที่คล้ายกับที่พบในแอป iOS และ Android
ในโพสต์นี้ ผมอยากจะแชร์ความคิดเกี่ยวกับการสร้างคอมโพเนนต์แท็บสำหรับเว็บ ซึ่งตอบสนองได้ รองรับอินพุตอุปกรณ์หลายรายการ และทำงานในเบราว์เซอร์ต่างๆ ทดลองใช้การสาธิต
หากชอบวิดีโอ นี่คือโพสต์นี้เวอร์ชัน YouTube
ภาพรวม
แท็บเป็นองค์ประกอบที่พบได้ทั่วไปของระบบการออกแบบแต่อาจมีรูปร่างและรูปแบบที่หลากหลาย ตอนแรกมีแท็บบนเดสก์ท็อปที่สร้างจากองค์ประกอบ <frame>
และตอนนี้เรามีคอมโพเนนต์บนอุปกรณ์เคลื่อนที่แบบเนยๆ ที่สร้างภาพเคลื่อนไหวตามสมบัติทางฟิสิกส์
ทุกคนพยายามทำสิ่งเดียวกัน นั่นก็คือประหยัดพื้นที่
ปัจจุบันนี้ องค์ประกอบสำคัญของประสบการณ์ของผู้ใช้แท็บคือพื้นที่สำหรับการนำทางด้วยปุ่มที่สลับการแสดงเนื้อหาในเฟรมแสดงผล พื้นที่สำหรับเนื้อหาที่แตกต่างกันจำนวนมากใช้พื้นที่เดียวกัน แต่นำเสนออย่างมีเงื่อนไขตามปุ่มที่เลือกในการนำทาง
![ภาพคอลลาจค่อนข้างวุ่นวายเนื่องจากรูปแบบที่หลากหลายที่เว็บนำมาใช้กับแนวคิดของคอมโพเนนต์](https://web.dev/static/articles/building/a-tabs-component/image/the-collage-is-quite-chao-b6f8cd38f8ddd.png?authuser=6&hl=th)
กลยุทธ์สำหรับเว็บ
โดยรวมแล้ว ผมพบว่าองค์ประกอบนี้ค่อนข้างตรงไปตรงมา เนื่องจากฟีเจอร์แพลตฟอร์มเว็บที่สำคัญ 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 กล่องที่มีลูกศรชี้ทิศทางจับคู่สีซึ่งแสดงเส้นขอบพื้นที่การเลื่อนและแสดงทิศทางที่จะเลื่อน](https://web.dev/static/articles/building/a-tabs-component/image/3-colorful-boxes-color-m-da0de23b13551.png?authuser=6&hl=th)
การเลื่อนมีองค์ประกอบ 2 ประเภทดังนี้
- หน้าต่าง
ช่องที่มีมิติข้อมูลที่กำหนดไว้ซึ่งมีสไตล์พร็อพเพอร์ตี้overflow
- แพลตฟอร์มที่เกินขนาด
ในเลย์เอาต์นี้ จะเป็นคอนเทนเนอร์รายการ ซึ่งได้แก่ ลิงก์การนำทาง บทความในส่วน และเนื้อหาบทความ
เลย์เอาต์ <snap-tabs>
เลย์เอาต์ระดับบนสุดที่ฉันเลือกคือ Flex (Flexbox) ฉันตั้งค่าทิศทางเป็น column
ส่วนหัวและส่วนจะอยู่ในแนวตั้ง นี่เป็นหน้าต่างแบบเลื่อนแรกของเรา และมันซ่อนทุกอย่างโดยซ่อนรายการเพิ่มเติมไว้ ส่วนหัวและส่วน
จะใช้การเลื่อนเกินเร็วๆ นี้ เนื่องจากแยกเป็นแต่ละโซน
<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 ช่วยให้เราเห็นหน้าต่าง ที่คอนเทนเนอร์การเลื่อนสร้างขึ้น
![องค์ประกอบส่วนหัวและส่วนจะมีการวางซ้อน Hotpink อยู่ ซึ่งจะช่วยระบุพื้นที่ว่างที่ใช้ในคอมโพเนนต์](https://web.dev/static/articles/building/a-tabs-component/image/the-header-section-eleme-dd313d7eb1453.png?authuser=6&hl=th)
เลย์เอาต์ของแท็บ <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
ควรกระจายในแนวนอนพร้อมกับกลุ่มลิงก์ และเลย์เอาต์ของส่วนหัวนี้จะช่วยตั้งค่าขั้นตอนดังกล่าว ไม่มีองค์ประกอบที่มีตำแหน่งแน่นอนที่นี่
![องค์ประกอบ nav และ span.indicator มีการซ้อนทับ Hotpink บนนั้น ซึ่งจะระบุพื้นที่ว่างที่เกิดขึ้นในคอมโพเนนต์](https://web.dev/static/articles/building/a-tabs-component/image/the-nav-spanindicator-e-eddaf5b04df39.png?authuser=6&hl=th)
ต่อไปคือรูปแบบการเลื่อน ปรากฏว่าเราสามารถแชร์รูปแบบการเลื่อนระหว่างพื้นที่เลื่อนแนวนอน 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
<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; } }
แต่ละรูปแบบและขนาดลิงก์นั้นเอง ดังนั้นเลย์เอาต์การนำทางจึงต้องระบุเพียงทิศทางและขั้นตอนเท่านั้น ความกว้างที่ไม่ซ้ำกันในรายการการนำทางทำให้การเปลี่ยนระหว่างแท็บดูสนุก เนื่องจากตัวบ่งชี้จะปรับความกว้างของแท็บให้สอดคล้องกับเป้าหมายใหม่ เบราว์เซอร์จะแสดงแถบเลื่อนหรือไม่ ทั้งนี้ขึ้นอยู่กับจำนวนองค์ประกอบที่มีอยู่ในส่วนนี้
![องค์ประกอบในการนำทางจะมีการวางซ้อนสี Hotpink บนองค์ประกอบนั้น โดยเน้นพื้นที่ที่ใช้ภายในองค์ประกอบ รวมถึงตำแหน่งที่ล้น](https://web.dev/static/articles/building/a-tabs-component/image/the-elements-the-nav-h-c8efe2b10b412.png?authuser=6&hl=th)
เลย์เอาต์ของแท็บ <section>
ส่วนนี้เป็นรายการแบบยืดหยุ่น และต้องเป็นผู้บริโภคพื้นที่หลัก นอกจากนี้คุณจะต้องสร้างคอลัมน์สำหรับวางบทความด้วย ขอย้ำอีกครั้งว่าการใช้
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
บอกตารางกริดให้จัดวางเด็กไว้ในเส้นแนวนอนเสมอ ไม่มีการห่อหุ้ม ตรงที่เราต้องการ เพื่อให้ล้นหน้าต่างระดับบนสุด
![องค์ประกอบของบทความจะมีการวางซ้อน Hotpink อยู่ โดยอธิบายพื้นที่ว่างที่ใช้ในคอมโพเนนต์และส่วนที่จะล้น](https://web.dev/static/articles/building/a-tabs-component/image/the-article-elements-hot-924ae524ee8fb.png?authuser=6&hl=th)
ฉันว่าบางทีมันก็ยากที่จะก้มหน้าก้มตา องค์ประกอบของส่วนนี้พอดี แต่ก็สร้างกล่องหลายกล่องด้วย เราหวังว่าภาพและ คำอธิบายจะช่วยคุณได้
เลย์เอาต์ของแท็บ <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; }
ฉันจึงเลือกให้บทความต่างๆ รวมอยู่ในแถบเลื่อนของผู้ปกครอง ผมชอบวิธีที่รายการลิงก์การนำทางและองค์ประกอบของบทความสแนปไปยังจุดเริ่มต้นในบรรทัดของคอนเทนเนอร์แบบเลื่อนที่เกี่ยวข้องกัน ซึ่งมีรูปลักษณ์และความรู้สึก เป็นสายสัมพันธ์ที่เป็นหนึ่งเดียว
![องค์ประกอบของบทความและองค์ประกอบย่อยจะมีการวางซ้อน Hotpink อยู่ โดยเน้นพื้นที่ที่เกิดขึ้นในคอมโพเนนต์และทิศทางที่ล้น](https://web.dev/static/articles/building/a-tabs-component/image/the-article-element-its-8afcb8a6817ff.png?authuser=6&hl=th)
บทความจะเป็นตารางกริดย่อยและมีการกำหนดขนาดไว้ล่วงหน้าให้เป็นพื้นที่วิวพอร์ตที่เราต้องการให้ UX ในการเลื่อน หมายความว่าผมไม่ต้องการรูปแบบความสูงหรือความกว้างที่นี่ ผมแค่ต้องกำหนดว่าจะให้แตกต่างกันอย่างไร ผมตั้งค่า overflow-y ให้เป็น "อัตโนมัติ" แล้วดักจับการโต้ตอบการเลื่อนด้วยพร็อพเพอร์ตี้ลักษณะการทำงานของการเลื่อนมากเกินไป
สรุปพื้นที่การเลื่อน 3 ส่วน
ด้านล่าง ฉันได้เลือก "แสดงแถบเลื่อนเสมอ" ในการตั้งค่าระบบ ฉันคิดว่าสิ่งสำคัญคือการทำให้เลย์เอาต์ทำงานได้เมื่อเปิดการตั้งค่านี้ไว้ เพราะต้องตรวจสอบเลย์เอาต์และการจัดการกระบวนการเลื่อน
![แถบเลื่อน 3 แถบถูกตั้งค่าให้แสดง ตอนนี้ใช้พื้นที่ของเลย์เอาต์และคอมโพเนนต์ของเรายังคงดูดีอยู่](https://web.dev/static/articles/building/a-tabs-component/image/the-3-scrollbars-are-set-ca02ac51f046d.png?authuser=6&hl=th)
ผมคิดว่าเมื่อเห็นรางเลื่อนของแถบเลื่อนในคอมโพเนนต์นี้ จะช่วยให้เห็นได้ชัดเจนว่าพื้นที่ในการเลื่อนอยู่ที่ไหน ทิศทางที่รองรับ และมีการประสานงานกันอย่างไร ลองพิจารณาว่าเฟรมหน้าต่างแบบเลื่อนแต่ละเฟรมเหล่านี้เป็น Flex หรือตารางกริดหลักสำหรับเลย์เอาต์
เครื่องมือสำหรับนักพัฒนาเว็บช่วยให้เราเห็นภาพของสิ่งต่อไปนี้ได้
![พื้นที่เลื่อนมีการวางซ้อนเครื่องมือแบบตารางกริดและ Flexbox เพื่อสรุปพื้นที่ที่ใช้ในการเลื่อนคอมโพเนนต์และทิศทางที่ล้นออก](https://web.dev/static/articles/building/a-tabs-component/image/the-scroll-areas-grid-f-79f0ae5839bcc.png?authuser=6&hl=th)
เลย์เอาต์การเลื่อนเสร็จสมบูรณ์แล้ว ทั้งการสแนป ทำ 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 ซึ่งแสดงคะแนนคอนทราสต์ที่ผ่านสำหรับทั้ง](https://web.dev/static/articles/building/a-tabs-component/image/active-tab-inactive-tab-9604191ea7007.png?authuser=6&hl=th)
ผู้ใช้ขับเคลื่อนภาพเคลื่อนไหวด้วยการโต้ตอบ จะเห็นความกว้างและตำแหน่งของสัญญาณบอกสถานะเปลี่ยนไปจากส่วนหนึ่งไปยังอีกส่วน ซึ่งติดตามได้อย่างสมบูรณ์แบบด้วยการเลื่อน
คุณอาจไม่เคยสังเกตมาก่อน แต่ฉันภูมิใจมากกับการเปลี่ยนสีเมื่อเลือกรายการการนำทางที่มีการไฮไลต์
สีเทาอ่อนที่ไม่ได้เลือกจะถอยกลับมากขึ้นอีกเมื่อรายการที่ไฮไลต์มีคอนทราสต์มากขึ้น เป็นเรื่องปกติที่จะเปลี่ยนสีสำหรับข้อความ เช่น เมื่อวางเมาส์เหนือ และเมื่อเลือก แต่การเปลี่ยนสีนั้นเป็นอีกระดับหนึ่งในการเปลี่ยนสีขณะที่เลื่อน ซึ่งซิงค์กับตัวระบุขีดเส้นใต้
นี่คือวิธีการที่ฉันทำ:
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 เป็นคำที่ใช้เรียกอุปกรณ์เคลื่อนที่มากกว่า แต่ผมคิดว่าการทำงานของ 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 ซึ่งมีสล็อตในเฟรมเวิร์กโปรด 🙂
มาลองเปลี่ยนแนวทางของเราและเรียนรู้วิธีทั้งหมดเพื่อสร้างเว็บกันเถอะ สร้างกลิทช์ ทวีตฉัน ไปที่เวอร์ชันของคุณ แล้วฉันจะเพิ่มไปยังส่วนรีมิกซ์ของชุมชนด้านล่าง
รีมิกซ์ของชุมชน
- @devnook, @rob_dodson และ @DasSurma ที่มีคอมโพเนนต์ของเว็บ: บทความ
- @jhvanderschee พร้อมปุ่ม: Codepen