انتقل إلى المحتوى
uiify

Virtual scroll

قائمة تعرض نافذة من عناصرها — عشرة آلاف صف بنحو خمس وعشرين عقدة DOM.

المتصفحات
Chrome 123+Firefox 121+Safari 17.5+
الفئة
2
الحجم المضغوط
4.16 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
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);
    corner-shape: squircle;
    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
          [corner-shape:squircle] 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
          [corner-shape:squircle] 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>}
      />
    </>
  );
}
/* app.css */

@layer reset, ui, app, tokens, base, demo, components, utilities, theme;

@import "@gunkin/uiify/styles/reset";

@import "@gunkin/uiify/components/behavior.css";

@import "@gunkin/uiify/styles/tokens";

@import "@gunkin/uiify/styles/tailwind";

@import "tailwindcss/utilities.css";

@theme { --spacing: 0.25rem; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; --font-weight-bold: 700; }

@source inline('grid gap-md w-max max-w-[min(28rem,calc(100vw_-_2_*_var(--space-lg)))] p-6 border border-border rounded-xl [corner-shape:squircle] 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)]');

@layer base { :root { --uiify-control-text: var(--text-sm); } @media (pointer: coarse) { :root { --uiify-control-text: var(--text-base); } } }

/* Authored animation and registered properties used by the Tailwind skin.
 * Visual rules belong in utility recipes; only at-rule definitions live here. */
@layer components {
  @keyframes uiify-presence-in {
    from {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@layer components {
  @keyframes uiify-presence-out {
    to {
      opacity: 0;
      scale: var(--presence-scale);
    }
  }
}

@property --uiify-spinner-tail {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@property --uiify-spinner-length {
  syntax: "<angle>";
  inherits: false;
  initial-value: 36deg;
}

@layer components {
  @keyframes uiify-spinner-rotate {
    to {
      transform: rotate(360deg);
    }
  }
}

@layer components {
  @keyframes uiify-spinner-orbit {
    0% {
      --uiify-spinner-tail: 0deg;
      --uiify-spinner-length: 36deg;
    }

    50% {
      --uiify-spinner-tail: 90deg;
      --uiify-spinner-length: 324deg;
    }

    100% {
      --uiify-spinner-tail: 360deg;
      --uiify-spinner-length: 36deg;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-pulse {
    50% {
      opacity: 0.45;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-shimmer {
    to {
      background-position: -200% 0;
    }
  }
}

@layer components {
  @keyframes uiify-skeleton-reveal {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
}

@property --candle-body-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-body-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-slot {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 100%;
}

@property --candle-wick-height {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-wick-top {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --candle-x {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 0%;
}

@property --candle-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@property --line-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-prev-y {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 50%;
}

@property --line-count {
  syntax: "<integer>";
  inherits: false;
  initial-value: 1;
}

@property --line-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

@layer components {
  @keyframes uiify-otp-input-caret-blink {
    50% {
      opacity: 0;
    }
  }
}

API

VirtualScroll

الخاصيةالنوعافتراضيالوصف
items*T[]—قائمة العناصر كاملة، ولكل عنصر معرّف ثابت؛ ولا تُعرض إلا الصفوف في النطاق الحالي.
renderRow*(item: T, index: number) => ReactNode—تعرض محتويات صف واحد بناءً على عنصره وفهرسه المطلق.
heightnumber—ارتفاع صريح بالبكسل. احذفه ومرّر style={{ blockSize: '100%' }} لملء ارتفاع محدّد للأب؛ والافتراضي ستمئة بكسل إن لم يُمرّر أي منهما.
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—الفئة المطبّقة على حاوية التمرير.
testIdstring—قيمة سمة data-testid لحاوية التمرير.

لوحة المفاتيح

المفتاحالإجراء
Scrollتحريك النافذة بعجلة الفأرة أو لوحة اللمس أو شريط التمرير
Tabنقل التركيز بين عناصر تحكم الصف (مربع الاختيار وزر تفعيل القائمة)
Arrow keysالتنقل بين العناصر داخل قائمة منسدلة مفتوحة
Escapeإغلاق قائمة منسدلة مفتوحة

إمكانية الوصول

يحافظ فاصل بارتفاع كامل على دقة شريط التمرير الأصلي، بينما لا تُثبّت إلا مجموعة ثابتة من خانات الصفوف، مع صفوف إضافية خارج الجزء المرئي؛ وتعيد مفاتيح الخانات الثابتة استخدام عقد DOM. تكشف الصفوف role="listitem" مع aria-posinset وaria-setsize داخل منفذ تمرير بدور role="list". تستخدم مزامنة التمرير مستمعًا بوضع passive، وتحدّث القفزات الكبيرة، مثل استخدام شريط التمرير أو Home/End أو Cmd+Arrow، النطاق تزامنيًا.

نقاط التنسيق

data-uiify-virtual-scrolldata-part="spacer"data-part="row"
تُرجمت هذه الصفحة آليًا ولم تُراجع بعد.