דילוג לתוכן
uiify

Virtual scroll

רשימה בחלון תצוגה — 10k שורות, כ-25 צומתי DOM.

דפדפנים
Chrome 123+Firefox 121+Safari 17.5+
שכבה
2
גודל חבילה דחוסה
4.81 kB
מגבלת פריסה כפויה
0 ms
מגבלת Axe
0
תואם SSR
כן
חוזה מפורסם
סביבת בדיקות

מגרש ניסויים

Virtual scroll

10,000 team members with checkbox, avatar, and dropdown actions. Only visible rows (+ buffer) mount in the DOM — open DevTools and count [data-uiify-virtual-scroll] [data-part="row"] nodes while scrolling.

User 1
Admin
User 2
Viewer
User 3
Editor
User 4
Admin
User 5
Editor
User 6
Viewer
User 7
Admin
User 8
Viewer
User 9
Editor
User 10
Admin
User 11
Editor
User 12
Viewer
User 13
Admin
User 14
Viewer
User 15
Editor
User 16
Admin
User 17
Editor
User 18
Viewer

Horizontal

The same users as a horizontal strip with orientation="horizontal". rowHeight sets each card's width; the window follows the viewport width.

User 1
Admin
User 2
Viewer
User 3
Editor
User 4
Admin
User 5
Editor
User 6
Viewer
User 7
Admin
User 8
Viewer
User 9
Editor
User 10
Admin
User 11
Editor
User 12
Viewer
User 1
Admin
User 2
Viewer
User 3
Editor
User 4
Admin
User 5
Editor
User 6
Viewer
User 7
Admin
User 8
Viewer
User 9
Editor
User 10
Admin
User 11
Editor
User 12
Viewer
import { VirtualScroll } from "@gunkin/uiify";

const items = Array.from({ length: 10000 }, (_, index) => ({
  id: String(index),
  label: "Row " + (index + 1),
}));

export default function Example() {
  return (
    <>
      <VirtualScroll className="" height={400} items={items} renderRow={(item) => <div>{item.label}</div>} />
      {/* rowHeight is the item size along the scroll axis — here, each card's width. */}
      <VirtualScroll
        className=""
        height={120}
        items={items}
        orientation="horizontal"
        rowHeight={144}
        renderRow={(item) => <div>{item.label}</div>}
      />
    </>
  );
}
/* Generic skin for custom core surfaces used by the demo and consumers. */

@layer components {
  [data-uiify-surface] {
    display: grid;
    gap: var(--space-md);
    inline-size: max-content;
    max-inline-size: min(28rem, calc(100vw - 2 * var(--space-lg)));
    padding: var(--space-lg);
    border: var(--border-width) solid var(--border);
    border-radius: var(--radius-xl);
    background-color: var(--card);
    color: var(--card-foreground);
    box-shadow: var(--shadow-popover);
  }

  [data-uiify-surface][popover]:not(:popover-open) {
    display: none;
  }

  @media (prefers-reduced-motion: no-preference) {
    [data-uiify-surface][popover] {
      transition: var(--transition-presence);
    }

    [data-uiify-surface][popover]:not(:popover-open) {
      opacity: 0;
      scale: var(--presence-scale);
    }

    @starting-style {
      [data-uiify-surface][popover]:popover-open {
        opacity: 0;
        scale: var(--presence-scale);
      }
    }
  }
}
import { VirtualScroll } from "@gunkin/uiify";

const items = Array.from({ length: 10000 }, (_, index) => ({
  id: String(index),
  label: "Row " + (index + 1),
}));

export default function Example() {
  return (
    <>
      <VirtualScroll
        className="
          grid gap-md w-max max-w-[min(28rem,calc(100vw_-_2_*_var(--space-lg)))] p-6 border border-border rounded-xl
          bg-card text-card-foreground shadow-popover [&[popover]:not(:popover-open)]:hidden
          motion-safe:[&[popover]]:[transition:var(--transition-presence)]
          motion-safe:[&[popover]:not(:popover-open)]:opacity-0
          motion-safe:[&[popover]:not(:popover-open)]:[scale:var(--presence-scale)]
          motion-safe:starting:[&[popover]:popover-open]:opacity-0
          motion-safe:starting:[&[popover]:popover-open]:[scale:var(--presence-scale)]
        "
        height={400}
        items={items}
        renderRow={(item) => <div>{item.label}</div>}
      />
      {/* rowHeight is the item size along the scroll axis — here, each card's width. */}
      <VirtualScroll
        className="
          grid gap-md w-max max-w-[min(28rem,calc(100vw_-_2_*_var(--space-lg)))] p-6 border border-border rounded-xl
          bg-card text-card-foreground shadow-popover [&[popover]:not(:popover-open)]:hidden
          motion-safe:[&[popover]]:[transition:var(--transition-presence)]
          motion-safe:[&[popover]:not(:popover-open)]:opacity-0
          motion-safe:[&[popover]:not(:popover-open)]:[scale:var(--presence-scale)]
          motion-safe:starting:[&[popover]:popover-open]:opacity-0
          motion-safe:starting:[&[popover]:popover-open]:[scale:var(--presence-scale)]
        "
        height={120}
        items={items}
        orientation="horizontal"
        rowHeight={144}
        renderRow={(item) => <div>{item.label}</div>}
      />
    </>
  );
}

API

VirtualScroll

מאפייןטיפוסברירת מחדלתיאור
items*T[]—רשימת הפריטים המלאה, עם מזהה יציב לכל פריט; רק השורות בטווח הנוכחי מוצגות.
renderRow*(item: T, index: number) => ReactNode—מציגה את תוכן השורה לפי הפריט שלה והאינדקס המוחלט שלה.
slotPropsVirtualScrollSlotProps—מאפיינים מובנים למרווח ולעטיפות השורות שנוצרים; ניתן למקד מאפייני שורות חוזרות לפי מזהה פריט יציב או אינדקס פריט מוחלט. מיקום השורה, התפקיד וסמני הפריסה נשארים בשליטת הספרייה.
heightnumber—גובה מפורש בפיקסלים. ניתן להשמיט אותו ולהעביר style={{ blockSize: '100%' }} כדי למלא גובה מוגדר של ההורה; ברירת המחדל היא 600px אם אף אחד מהם לא סופק.
rowHeightnumber64גודל קבוע בפיקסלים לכל שורה בציר הגלילה (גובה בגלילה אנכית, רוחב באופקית), המשמש לחישוב גודל הגלילה והשורות הגלויות.
orientation"vertical" | "horizontal""vertical"ציר הגלילה. רשימות אופקיות נגללות בציר השורה, קובעות את גודל החלון לפי רוחב אזור התצוגה הנמדד, מתהפכות כראוי בפריסות מימין לשמאל ומשקפות את הערך ב-data-orientation.
overscannumber5מספר השורות הנוספות המוצגות מעל לטווח הגלוי ומתחתיו, כדי שגלילה מהירה לא תחשוף שטח ריק.
estimateRowHeight(index: number) => number—אומדן גובה לכל אינדקס, המעביר את הרשימה לפריסה דינמית שבה השורות המוצגות נמדדות והאומדן מתוקן.
role"list" | "grid""list"תפקיד ARIA המוחל על מכל הגלילה.
anchor"start" | "end""start"קצה הרשימה שאליו נשאר מיקום הגלילה מוצמד כשמוסיפים או מסירים פריטים.
followAppendboolean | "smooth"falseגולל לפריטים חדשים שנוספו כשהרשימה כבר נמצאת בסופה, עם אפשרות לגלילה חלקה.
scrollRefRef<VirtualScrollHandle>—מקבל את ממשק השליטה החושף scrollToIndex, scrollToOffset ו-scrollToEnd.
onRangeChange(range: VirtualScrollRangeChange) => void—נקראת עם טווח האינדקסים הגלוי והטווח המורחב המוצג מראש בכל תזוזה של חלון התצוגה.
onScrollStateChange(state: VirtualScrollScrollState) => void—נקראת כשהגלילה מתחילה וכשהיא מסתיימת.
isScrollingResetDelaynumber150מספר אלפיות השנייה ללא פעילות שאחריהן הגלילה נחשבת כמסתיימת בדפדפנים ללא האירוע scrollend.
classNamestring—המחלקה המוחלת על מכל הגלילה.
styleCSSProperties—סגנונות אלמנט השורש המובנה.
refRef<HTMLDivElement>—ה-ref המועבר לאלמנט השורש המוצג.
testIdstring—הערך של המאפיין data-testid במכל הגלילה.

מקלדת

מקשפעולה
Scrollהגלגלת, משטח המגע או פס הגלילה מזיזים את חלון התצוגה
Tabהעברת המיקוד בין פקדי השורה (תיבת סימון, מפעיל תפריט)
Arrow keysמעבר בין פריטים בתוך תפריט נפתח פתוח
Escapeסגירת תפריט נפתח פתוח

נגישות

רכיב מרווח בגובה מלא שומר על דיוק פס הגלילה המובנה, בעוד שרק מאגר קבוע של תאי שורות, בתוספת שורות מעבר לתצוגה, מותקן; מפתחות תאים יציבים ממחזרים צומתי DOM. השורות חושפות role="listitem" עם aria-posinset ו-aria-setsize בתוך אזור גלילה עם role="list". סנכרון הגלילה פסיבי, וקפיצות גדולות באמצעות פס הגלילה, Home/End או Cmd+Arrow מעדכנות את הטווח באופן סינכרוני.

נקודות חיבור לעיצוב

data-uiify-virtual-scrolldata-part="spacer"data-part="row"
דף זה תורגם אוטומטית וטרם נבדק.