בניית רכיב כרטיסיות

סקירה כללית בסיסית על אופן יצירת רכיב כרטיסיות דומה לזה שמופיע באפליקציות ל-iOS ול-Android.

בפוסט הזה אני רוצה לשתף את המחשבות שלי על יצירת רכיב כרטיסיות לאינטרנט, שהוא רספונסיבי, תומך בקלט ממכשירים שונים ועובד בדפדפנים שונים. כדאי לנסות את ההדגמה.

הדגמה

אם אתם מעדיפים לצפות בסרטון, הנה גרסת YouTube של הפוסט הזה:

סקירה כללית

כרטיסיות הן רכיב נפוץ במערכות עיצוב, אבל הן יכולות להיות בצורות שונות. היו כרטיסיות למחשב שמבוססות על רכיב <frame>, ועכשיו יש רכיבים חלקים לנייד שמנפישים תוכן על סמך מאפיינים פיזיקליים. כולם מנסים לעשות את אותו הדבר: לחסוך מקום.

כיום, המרכיבים החיוניים של חוויית המשתמש בכרטיסיות הם אזור ניווט עם לחצנים שמאפשר להחליף את התוכן שמוצג במסגרת. אזורי תוכן רבים חולקים את אותו המקום, אבל הם מוצגים באופן מותנה על סמך הלחצן שנבחר בניווט.

הקולאז&#39; די כאוטי בגלל המגוון העצום של סגנונות שמוחלים באינטרנט על קונספט הרכיבים
קולאז' של סגנונות עיצוב אתרים של רכיבי כרטיסיות מ-10 השנים האחרונות

Web Tactics

בסך הכול, היה לי די קל ליצור את הרכיב הזה, בזכות כמה תכונות חשובות של פלטפורמת האינטרנט:

  • scroll-snap-points לאינטראקציות אלגנטיות של החלקה ומקלדת עם מיקומי עצירה מתאימים של הגלילה
  • קישורי עומק באמצעות גיבוב של כתובות URL לדפדפן, עם תמיכה בקיבוע גלילה בדף ובשיתוף
  • תמיכה בקורא מסך עם תגי עיצוב של רכיבי <a> ו-id="#hash"
  • prefers-reduced-motion כדי להפעיל מעברים של החלפה הדרגתית וגלילה מיידית בדף
  • תכונת האינטרנט @scroll-timeline שנמצאת בשלב הטיוטה ומאפשרת להוסיף קו תחתון באופן דינמי לכרטיסייה שנבחרה ולשנות את הצבע שלה

קוד ה-HTML

בבסיס, חוויית המשתמש כאן היא: לחיצה על קישור, כתובת ה-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. משטח גדול מדי
    בפריסה הזו, אלה מאגרי הרשימות: nav links,‏ section articles ו-article contents.

פריסה של <snap-tabs>

פריסת הרמה העליונה שבחרתי הייתה flex ‏ (Flexbox). הגדרתי את הכיוון ל-column, כך שהכותרת והקטע מסודרים אנכית. זהו חלון הגלילה הראשון שלנו, והוא מסתיר את כל מה שחורג ממנו. בקרוב נתחיל להשתמש ב-overscroll בכותרת ובקטע, כאזורים נפרדים.

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 מודגשים בורוד לוהט, ומוצג מתאר של השטח שהם תופסים ברכיב

אחר כך, סגנונות הגלילה. התברר שאפשר לשתף את סגנונות הגלילה בין שני אזורי הגלילה האופקית (הכותרת והקטע), אז יצרתי מחלקה של כלי עזר, .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;
    }
  }
}

לכל אחד מהם צריך להגדיר גלילה אופקית, גלילה מוגבלת כדי למנוע גלילה מעבר לקצה, פסי גלילה מוסתרים למכשירי מגע, ובסוף גלילה עם הצמדה כדי לנעול את אזורי הצגת התוכן. סדר הכרטיסיות במקלדת שלנו נגיש, וכל האינטראקציות מנחות את המיקוד באופן טבעי. בנוסף, קונטיינרים של גלילה עם הצמדה מקבלים אינטראקציה בסגנון קרוסלה מהמקלדת.

פריסה של כותרת הכרטיסיות <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;
  }
}

כל קישור מעצב את עצמו וקובע את הגודל שלו, כך שפריסת הניווט צריכה לציין רק את הכיוון והזרימה. הרוחב הייחודי של פריטי הניווט הופך את המעבר בין הכרטיסיות למהנה, כי האינדיקטור מתאים את הרוחב שלו ליעד החדש. בהתאם למספר הרכיבים שמופיעים כאן, הדפדפן יציג או לא יציג סרגל גלילה.

רכיבי ה-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;
}

בחרתי שהמאמרים יתמקמו בתוך אזור הגלילה של הרכיב ההורה. מאוד אהבתי את האופן שבו הפריטים של קישורי הניווט והרכיבים של המאמר נצמדים לתחילת השורה של מאגרי הגלילה שלהם. נראה שיש ביניהם יחסים הרמוניים.

רכיב המאמר ורכיבי הצאצא שלו מודגשים בקווי מתאר בצבע ורוד בוהק, שמציינים את השטח שהם תופסים ברכיב ואת הכיוון שבו הם חורגים מהגבולות

המאמר הוא צאצא של רשת, והגודל שלו נקבע מראש להיות אזור חלון התצוגה שבו אנחנו רוצים לספק חוויית משתמש של גלילה. המשמעות היא שלא צריך להגדיר כאן סגנונות של גובה או רוחב, אלא רק להגדיר איך הגלישה תתבצע. הגדרתי את overflow-y ל-auto, ואז גם לכדתי את אינטראקציות הגלילה באמצעות המאפיין הנוח overscroll-behavior.

סיכום של 3 אזורים שניתנים לגלילה

למטה בחרתי בהגדרות המערכת באפשרות 'הצגת פסי גלילה תמיד'. לדעתי, חשוב במיוחד שהפריסה תפעל כשההגדרה הזו מופעלת, כי אני צריך לבדוק את הפריסה ואת תיאום הגלילה.

הגדרנו ש-3 פסי הגלילה יוצגו, ועכשיו הם תופסים מקום בפריסה, אבל הרכיב שלנו עדיין נראה מצוין

אני חושב שכשרואים את שולי פס הגלילה ברכיב הזה, קל להבין איפה נמצאים אזורי הגלילה, באיזה כיוון הם תומכים ואיך הם פועלים יחד. כדאי לחשוב על כך שכל אחת ממסגרות חלונות הגלילה האלה היא גם פריט אב של פריסת flex או grid.

כלי הפיתוח יכולים לעזור לנו להמחיש את זה:

באזורי הגלילה יש שכבות-על של כלי רשת ו-Flexbox, שמתוות את המקום שהם תופסים ברכיב ואת הכיוון שבו הם חורגים מהגבולות
כלי פיתוח ל-Chromium, שבו מוצג פריסת אלמנט הניווט flexbox מלא באלמנטים מסוג עוגן, פריסת קטע הרשת מלא באלמנטים מסוג מאמר, ואלמנטים מסוג מאמר מלאים בפסקאות ובאלמנט כותרת.

פריסות הגלילה הושלמו: הצמדה, קישורי עומק וגישה באמצעות מקלדת. בסיס חזק לשיפורים בחוויית המשתמש, בסגנון ובאפקט הוואו.

תכונות מומלצות

כשמשנים את הגודל של חלונות צמודים, הם נשארים צמודים ולא משנים את המיקום שלהם. כלומר, לא צריך להשתמש ב-JavaScript כדי להציג משהו כשמסובבים את המכשיר או משנים את הגודל של חלון הדפדפן. כדי לנסות את התכונה הזו בכלי הפיתוח ל-Chromium, בוחרים באפשרות מצב מכשיר, בוחרים במצב כלשהו מלבד רספונסיבי, ואז משנים את הגודל של מסגרת המכשיר. שימו לב שהאלמנט נשאר בתצוגה ונעול עם התוכן שלו. האפשרות הזו זמינה מאז ש-Chromium עדכן את ההטמעה שלו כך שתתאים למפרט. בפוסט הזה בבלוג יש מידע נוסף על כך.

Animation

המטרה של עבודת האנימציה כאן היא לקשר באופן ברור בין אינטראקציות לבין משוב בממשק המשתמש. כך אפשר להנחות את המשתמש או לעזור לו למצוא את כל התוכן בצורה חלקה (בתקווה). אני אוסיף תנועה עם מטרה ובתנאי. עכשיו המשתמשים יכולים לציין את ההעדפות שלהם לגבי תנועה במערכת ההפעלה, ואני נהנה מאוד להגיב להעדפות שלהם בממשקים שלי.

אקשר בין קו תחתון בכרטיסייה לבין מיקום הגלילה במאמר. הצמדה היא לא רק יישור יפה, אלא גם עיגון של ההתחלה והסוף של אנימציה. כך <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 יודעת איך לקבל את המידע הזה, ולכן נחזור על הילדים ונשלוף את הערכים המחושבים בזמן הריצה:

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) מדגישה את הקישור, ובכל מקרה זה צבע טקסט רגיל. הלולאה המקוננת שם הופכת את זה לפשוט יחסית, כי הלולאה החיצונית היא כל פריט ניווט, והלולאה הפנימית היא כל מסגרת מפתח אישית של פריט ניווט. אני בודק אם הרכיב של הלולאה החיצונית זהה לזה של הלולאה הפנימית, ומשתמש בזה כדי לדעת מתי הוא נבחר.

נהניתי מאוד לכתוב את זה. כל כך הרבה.

שיפורים נוספים ב-JavaScript

חשוב לזכור שהליבה של מה שאני מראה לכם כאן פועלת בלי JavaScript. אחרי שאמרנו את זה, בואו נראה איך אפשר לשפר את התוצאה כשה-JS זמין.

קישורי עומק הם יותר מונח שקשור לניידים, אבל אני חושב שהכוונה של קישור עומק מתקיימת כאן באמצעות כרטיסיות, כי אפשר לשתף כתובת URL ישירות לתוכן של כרטיסייה. הדפדפן ינווט בתוך הדף למזהה שתואם לגיבוב של כתובת ה-URL. גיליתי שה-handler הזה 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); });

בכל פעם שמגללים את הקטעים, צריך לנקות את פרק הזמן הקצוב לתפוגה של הקטע אם הוא קיים, ולהתחיל פרק זמן חדש. כשמפסיקים לגלול בקטע, לא מבטלים את פסק הזמן ומפעילים את האירוע 100ms אחרי ההפסקה. כשהאירוע מופעל, קוראים לפונקציה שמנסה להבין איפה המשתמש הפסיק.

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 יכול לנהל את חוויית המשתמש באופן הצהרתי. לפעמים הן יוצרות התאמה קטנה ומשמחת.

סיכום

עכשיו שאתם יודעים איך עשיתי את זה, איך אתם הייתם עושים את זה?! כך נוצרת ארכיטקטורה מעניינת של רכיבים. מי ייצור את הגרסה הראשונה עם משבצות במסגרת המועדפת שלו? 🙂

כדאי לגוון את הגישות שלנו וללמוד את כל הדרכים לבנות אתרים. אפשר ליצור Glitch, לצייץ לי את הגרסה שלך ואוסיף אותה לקטע Community remixes שבהמשך.

רמיקסים מהקהילה