Virtual List
A long list that renders only what is on screen, measures rows of any height and loads more near the end.
Rendering Virtual List…
Installation
Copy the source into your project with the CLI. It lands under @/components/ui and is yours to edit.
npx mlola-ui add virtual-listUsage
import { VirtualList } from "@/components/ui/virtual-list";
export default function Example() {
return (
<VirtualList
label="Events"
count={10000}
height={240}
estimateSize={36}
renderItem={(index) => <div>Event {index + 1}</div>}
/>
);
}Styling comes from the engine stylesheet and the nearest theme. Set it once at the root:
<html data-theme="graphite" data-mode="light">HTML
The markup the example above renders, at rest. The interaction lives in the React component; from another framework, set the attributes below as the state changes.
<div class="ml-virtual-list" style="height:240px" role="list" aria-label="Events" tabindex="0">
<div class="ml-virtual-list-space" style="height:360000px">
<div class="ml-virtual-list-row" data-index="0" role="listitem" aria-setsize="10000" aria-posinset="1" style="transform:translateY(0px)">
<div>Event 1</div>
</div>
<div class="ml-virtual-list-row" data-index="1" role="listitem" aria-setsize="10000" aria-posinset="2" style="transform:translateY(36px)">
<div>Event 2</div>
</div>
<div class="ml-virtual-list-row" data-index="2" role="listitem" aria-setsize="10000" aria-posinset="3" style="transform:translateY(72px)">
<div>Event 3</div>
</div>
<div class="ml-virtual-list-row" data-index="3" role="listitem" aria-setsize="10000" aria-posinset="4" style="transform:translateY(108px)">
<div>Event 4</div>
</div>
<div class="ml-virtual-list-row" data-index="4" role="listitem" aria-setsize="10000" aria-posinset="5" style="transform:translateY(144px)">
<div>Event 5</div>
</div>
<div class="ml-virtual-list-row" data-index="5" role="listitem" aria-setsize="10000" aria-posinset="6" style="transform:translateY(180px)">
<div>Event 6</div>
</div>
<div class="ml-virtual-list-row" data-index="6" role="listitem" aria-setsize="10000" aria-posinset="7" style="transform:translateY(216px)">
<div>Event 7</div>
</div>
<div class="ml-virtual-list-row" data-index="7" role="listitem" aria-setsize="10000" aria-posinset="8" style="transform:translateY(252px)">
<div>Event 8</div>
</div>
<div class="ml-virtual-list-row" data-index="8" role="listitem" aria-setsize="10000" aria-posinset="9" style="transform:translateY(288px)">
<div>Event 9</div>
</div>
<div class="ml-virtual-list-row" data-index="9" role="listitem" aria-setsize="10000" aria-posinset="10" style="transform:translateY(324px)">
<div>Event 10</div>
</div>
<div class="ml-virtual-list-row" data-index="10" role="listitem" aria-setsize="10000" aria-posinset="11" style="transform:translateY(360px)">
<div>Event 11</div>
</div>
<div class="ml-virtual-list-row" data-index="11" role="listitem" aria-setsize="10000" aria-posinset="12" style="transform:translateY(396px)">
<div>Event 12</div>
</div>
<div class="ml-virtual-list-row" data-index="12" role="listitem" aria-setsize="10000" aria-posinset="13" style="transform:translateY(432px)">
<div>Event 13</div>
</div>
<div class="ml-virtual-list-row" data-index="13" role="listitem" aria-setsize="10000" aria-posinset="14" style="transform:translateY(468px)">
<div>Event 14</div>
</div>
<div class="ml-virtual-list-row" data-index="14" role="listitem" aria-setsize="10000" aria-posinset="15" style="transform:translateY(504px)">
<div>Event 15</div>
</div>
<div class="ml-virtual-list-row" data-index="15" role="listitem" aria-setsize="10000" aria-posinset="16" style="transform:translateY(540px)">
<div>Event 16</div>
</div>
<div class="ml-virtual-list-row" data-index="16" role="listitem" aria-setsize="10000" aria-posinset="17" style="transform:translateY(576px)">
<div>Event 17</div>
</div>
<div class="ml-virtual-list-row" data-index="17" role="listitem" aria-setsize="10000" aria-posinset="18" style="transform:translateY(612px)">
<div>Event 18</div>
</div>
<div class="ml-virtual-list-row" data-index="18" role="listitem" aria-setsize="10000" aria-posinset="19" style="transform:translateY(648px)">
<div>Event 19</div>
</div>
<div class="ml-virtual-list-row" data-index="19" role="listitem" aria-setsize="10000" aria-posinset="20" style="transform:translateY(684px)">
<div>Event 20</div>
</div>
<div class="ml-virtual-list-row" data-index="20" role="listitem" aria-setsize="10000" aria-posinset="21" style="transform:translateY(720px)">
<div>Event 21</div>
</div>
<div class="ml-virtual-list-row" data-index="21" role="listitem" aria-setsize="10000" aria-posinset="22" style="transform:translateY(756px)">
<div>Event 22</div>
</div>
<div class="ml-virtual-list-row" data-index="22" role="listitem" aria-setsize="10000" aria-posinset="23" style="transform:translateY(792px)">
<div>Event 23</div>
</div>
</div>
</div>Classes and attributes
A class names each element; data-* and aria-* attributes carry its variant and state. The values listed are the ones the stylesheet draws; the default needs no attribute.
| Class | Attributes |
|---|---|
ml-virtual-list | — |
ml-virtual-list-space | — |
ml-virtual-list-row | — |
Accessibility and theming
- Semantic elements, accessible names and state relationships are part of the contract.
- Keyboard, pointer and touch paths are operable; focus stays visible in every theme.
- Light and dark modes pass WCAG AA contrast in all 5 canonical themes.
- Reduced motion and forced colors keep meaning and operation.