אקורדיון וטאבים: מדדי Core Web Vitals ונגישות

אקורדיונים וטאבים גורמים לעיתים קרובות לבעיות ביצועים ונגישות: אנימציות מובילות לשינויי פריסה, ותוכן מוסתר מאבד מיקומים בחיפוש. אנו בונים רכיבים אלה עם תשומת לב ל-Core Web Vitals ול-WCAG, ומיישמים רינדור בצד השרת ותכונות ARIA. הצוות שלנו מספק פתרון מלא—מביקורת ועד תמיכה שוטפת—המבטיח פעילות יציבה ואינדוקס תוכן.

פיתוח ותחזוקה של כל סוגי האתרים:

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

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

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
אקורדיון וטאבים: מדדי Core Web Vitals ונגישות
פשוט
~1 יום

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1050
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

כיצד להימנע מפגיעה ב-Core Web Vitals ובנגישות באקורדיונים ובטאבים

לעתים קרובות אנו נתקלים בפרויקטים שבהם אקורדיונים וטאבים מיושמים ללא התחשבות בביצועים ובנגישות. בעיות אופייניות: אנימציית גובה עם height: auto גורמת ל-Layout Shift, תוכן של פאנלים מוסתרים אינו ממו�索引, והיעדר תכונות ARIA הופך רכיבים לבלתי נגישים לקוראי מסך. התוצאה — LCP נמוך, בדיקת Lighthouse נכשלת, ואובדן של עד 30% מתעבורת המובייל. הניסיון שלנו מראה שיישום נכון של רכיבים אלה במתכונת turnkey אורך 2 עד 3 ימים וכולל: רינדור בצד השרת, סנכרון URL, סימון Schema.org ותמיכה מלאה ב-WCAG 2.1 AA. להלן פתרונות ספציפיים שאנו מיישמים בסביבת ייצור.

אילו בעיות אנו פותרים?

הימנעות מ-CLS באנימציית אקורדיון

שימוש ב-height: auto במעברי CSS גורם לחישוב מחדש של הפריסה. הפתרון הוא JavaScript המודד את scrollHeight ומגדיר ערך קבוע. השתמשנו בגישה זו ביותר מ-30 פרויקטים. חלופה היא grid-template-rows: 0fr / 1fr של CSS עם overflow: hidden. אך גרסת ה-JS מבטיחה אנימציה חלקה בכל הדפדפנים, כולל Safari ישן. שיטה זו מבטלת את CLS לחלוטין, ומשפרת את ציוני Cumulative Layout Shift ב-0.2 בממוצע.

הבטחת אינדוקס של תוכן טאבים

Google מאנדקס תוכן עם display: none, אך נותן לו משקל נמוך יותר. עבור תוכן קריטי ל-SEO, אנו משתמשים ב-visibility: hidden + position: absolute וברינדור בצד השרת של כל הפאנלים. השוואה: עם הסתרה בצד הלקוח, התוכן מקבל משקל של 0.3 מתוכן פתוח, בעוד שעם SSR הוא מקבל משקל מלא. בפרויקט אחד, זה הגדיל את נראות הדף ב-25% לפי Yandex.Webmaster. רינדור בצד השרת טוב פי 5 ל-SEO מאשר הסתרה בצד הלקוח.

נגישות מקלדת לטאבים

אנו מיישמים את התבנית role="tablist" עם ArrowLeft/Right, Home/End. ניהול הפוקוס נעשה באמצעות tabIndex. ללא זה, הרכיב נכשל ב-WCAG 2.1 AA. עם הגישה שלנו, ניווט מקלדת תואם לחלוטין ונבדק עם קוראי מסך.

איך אנחנו עושים את זה: טכנולוגיות ודוגמה מעשית

אנו משתמשים ב-React 18, Next.js 14, TypeScript, Tailwind. לאנימציה — מעברי CSS עם בקרת גובה ב-JS. דוגמת אקורדיון (בקיצור):

import { useState, useRef, useEffect } from 'react'

interface AccordionItem {
  id: string
  title: string
  content: React.ReactNode
}

function AccordionItem({
  item,
  isOpen,
  onToggle,
}: {
  item: AccordionItem
  isOpen: boolean
  onToggle: () => void
}) {
  const contentRef = useRef<HTMLDivElement>(null)
  const [height, setHeight] = useState(0)

  useEffect(() => {
    if (contentRef.current) {
      setHeight(isOpen ? contentRef.current.scrollHeight : 0)
    }
  }, [isOpen])

  return (
    <div className="accordion-item">
      <button
        className="accordion-item__trigger"
        onClick={onToggle}
        aria-expanded={isOpen}
        aria-controls={`accordion-content-${item.id}`}
        id={`accordion-btn-${item.id}`}
      >
        {item.title}
      </button>
      <div
        id={`accordion-content-${item.id}`}
        role="region"
        aria-labelledby={`accordion-btn-${item.id}`}
        style={{ height, overflow: 'hidden', transition: 'height 0.3s ease' }}
      >
        <div ref={contentRef} className="accordion-item__body">
          {item.content}
        </div>
      </div>
    </div>
  )
}

לטאבים, אנו תמיד מוסיפים סנכרון URL באמצעות import { useState, useRef, useEffect } from 'react' interface AccordionItem { id: string title: string content: React.ReactNode } function AccordionItem({ item, isOpen, onToggle }: { item: AccordionItem isOpen: boolean onToggle: () => void }) { const contentRef = useRef<HTMLDivElement>(null) const [height, setHeight] = useState(0) useEffect(() => { if (contentRef.current) { setHeight(isOpen ? contentRef.current.scrollHeight : 0) } }, [isOpen]) return ( <div className="accordion-item"> <button className="accordion-item__trigger" onClick={onToggle} aria-expanded={isOpen} aria-controls={`accordion-content-${item.id}`} id={`accordion-btn-${item.id}`} > {item.title} </button> <div id={`accordion-content-${item.id}`} role="region" aria-labelledby={`accordion-btn-${item.id}`} style={{ height, overflow: 'hidden', transition: 'height 0.3s ease' }} > <div ref={contentRef} className="accordion-item__body"> {item.content} </div> </div> </div> ) } וסימון Schema.org עבור FAQ. דוגמת טאבים עם ניווט מקלדת:

import { useState, useRef } from 'react'

interface Tab {
  id: string
  label: string
  content: React.ReactNode
}

export function Tabs({ tabs, defaultTab }: { tabs: Tab[]; defaultTab?: string }) {
  const [activeId, setActiveId] = useState(defaultTab ?? tabs[0]?.id)
  const tablistRef = useRef<HTMLDivElement>(null)

  function handleKeyDown(e: React.KeyboardEvent, currentIndex: number) {
    let newIndex = currentIndex
    if (e.key === 'ArrowRight') newIndex = (currentIndex + 1) % tabs.length
    else if (e.key === 'ArrowLeft') newIndex = (currentIndex - 1 + tabs.length) % tabs.length
    else if (e.key === 'Home') newIndex = 0
    else if (e.key === 'End') newIndex = tabs.length - 1
    else return
    e.preventDefault()
    setActiveId(tabs[newIndex].id)
    const tabEls = tablistRef.current?.querySelectorAll('[role="tab"]')
    ;(tabEls?.[newIndex] as HTMLElement)?.focus()
  }

  return (
    <div className="tabs">
      <div role="tablist" ref={tablistRef} className="tabs__list">
        {tabs.map((tab, i) => (
          <button
            key={tab.id}
            role="tab"
            id={`tab-${tab.id}`}
            aria-selected={activeId === tab.id}
            aria-controls={`tabpanel-${tab.id}`}
            tabIndex={activeId === tab.id ? 0 : -1}
            onClick={() => setActiveId(tab.id)}
            onKeyDown={e => handleKeyDown(e, i)}
            className={`tabs__tab ${activeId === tab.id ? 'tabs__tab--active' : ''}`}
          >
            {tab.label}
          </button>
        ))}
      </div>
      {tabs.map(tab => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`tabpanel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={activeId !== tab.id}
          tabIndex={0}
          className="tabs__panel"
        >
          {tab.content}
        </div>
      ))}
    </div>
  )
}

רינדור בצד השרת של כל הפאנלים פותר בעיות SEO. ב-Next.js, אנו משתמשים ב-useSearchParams בדף עם טאבים או מרנדרים את כל הפאנלים ב-HTML, ומסתירים אותם באמצעות CSS (לא import { useState, useRef } from 'react' interface Tab { id: string label: string content: React.ReactNode } export function Tabs({ tabs, defaultTab }: { tabs: Tab[]; defaultTab?: string }) { const [activeId, setActiveId] = useState(defaultTab ?? tabs[0]?.id) const tablistRef = useRef<HTMLDivElement>(null) function handleKeyDown(e: React.KeyboardEvent, currentIndex: number) { let newIndex = currentIndex if (e.key === 'ArrowRight') newIndex = (currentIndex + 1) % tabs.length else if (e.key === 'ArrowLeft') newIndex = (currentIndex - 1 + tabs.length) % tabs.length else if (e.key === 'Home') newIndex = 0 else if (e.key === 'End') newIndex = tabs.length - 1 else return e.preventDefault() setActiveId(tabs[newIndex].id) const tabEls = tablistRef.current?.querySelectorAll('[role="tab"]') ;(tabEls?.[newIndex] as HTMLElement)?.focus() } return ( <div className="tabs"> <div role="tablist" ref={tablistRef} className="tabs__list"> {tabs.map((tab, i) => ( <button key={tab.id} role="tab" id={`tab-${tab.id}`} aria-selected={activeId === tab.id} aria-controls={`tabpanel-${tab.id}`} tabIndex={activeId === tab.id ? 0 : -1} onClick={() => setActiveId(tab.id)} onKeyDown={e => handleKeyDown(e, i)} className={`tabs__tab ${activeId === tab.id ? 'tabs__tab--active' : ''}`} > {tab.label} </button> ))} </div> {tabs.map(tab => ( <div key={tab.id} role="tabpanel" id={`tabpanel-${tab.id}`} aria-labelledby={`tab-${tab.id}`} hidden={activeId !== tab.id} tabIndex={0} className="tabs__panel" > {tab.content} </div> ))} </div> ) } , אלא force-dynamic).

דוגמה נוספת: אקורדיון עם אנימציית CSS Grid
.accordion__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease;
}
.accordion__panel--open {
  grid-template-rows: 1fr;
}

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

תהליך העבודה

שלב משך תוצאה
ביקורת על היישום הנוכחי 1-2 שעות דוח על Lighthouse, WAVE, CWV
עיצוב הרכיב 2-4 שעות תכנית ARIA, עיצוב אנימציה
פיתוח (HTML+CSS+JS) 4-6 שעות רכיבים טבעיים
פיתוח עטיפת React 6-8 שעות רכיבים עם SSR, URL, Schema
בדיקות 2-4 שעות מקלדת, קורא מסך, בדיקת ביצועים
פריסה ותיעוד 2-4 שעות מיזוג Git, readme, הדרכת צוות

לוחות זמנים ומה כלול

רכיב לוח זמנים
אקורדיון פשוט (טבעי) 2-3 שעות
אקורדיון React עם אנימציה 4-6 שעות
מערכת טאבים עם URL ו-Schema 1-1.5 ימים
סט מלא (אקורדיון+טאבים) 2-3 ימים

כלול בעבודה:

  • קוד מקור של הרכיבים (TypeScript, CSS modules או Tailwind)
  • תיעוד על API ותכונות ARIA
  • אינטגרציה לפרויקט קיים (Git)
  • הדרכת צוות (שעה)
  • תמיכה למשך שבועיים לאחר הפריסה

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

טעויות אופייניות

  • שימוש ב-display:none לאנימציית גובה מוביל ל-CLS. עדיף להשתמש ב-JS.
  • שכחת position:absolute; visibility:hidden; height:0; overflow:hidden — השבתת אנימציה.
  • אי בדיקת מצב טעינה — רכיב hydrated עלול "להבהב".

השוואת גישות

קריטריון HTML טבעי (.accordion__panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.3s ease; } .accordion__panel--open { grid-template-rows: 1fr; } ) רכיב React (אנימציית JS)
זמן פיתוח שעה 4-6 שעות
אנימציה CSS בלבד (לא עובד ללא overflow: hidden) חלקה, כל סוג (חלקה פי 2 מ-CSS טבעי)
נגישות סמנטיקה מובנית דורש ARIA ידני
SEO ממו�索引 במלואו דורש SSR לתוכן מוסתר

HTML טבעי זול ונגיש יותר, אך פתרון React נותן שליטה מלאה על אנימציה וסנכרון URL.

מדריך שלב-אחר-שלב ליישום אקורדיון

  1. צור markup עם כותרות ותוכן.
  2. הוסף תכונות ARIA: prefers-reduced-motion, <details>.
  3. כתוב סקריפט לאנימציית גובה באמצעות interpolate-size.
  4. טפל במצב aria-expanded.
  5. בדוק ניווט מקלדת (Tab, Enter).
  6. בדוק עם קוראי מסך (NVDA, VoiceOver).

סיכום

רכיבים מיושמים כראוי לא רק משפרים את חוויית המשתמש אלא גם משפרים מדדי ביצועים. המהנדסים שלנו מוסמכים ב-WCAG 2.1 AA, ולכן אנו מבטיחים יישום ARIA נכון. 5+ שנות ניסיון בפיתוח אתרים, 30+ פרויקטים עם רכיבי חשיפה וממשקי טאבים — אנו מכירים את כל המלכודות. אם ברצונך להשיג תוצאות דומות, צור קשר להערכת פרויקט. לעיון נוסף, ראה את תפקיד tab ב-MDN. אנו נעריך את המשימה ונציע פתרון אופטימלי.