---
id: "packages/active-virtual/getting-started"
title: "Getting started"
description: "Receive active-virtual and render a first virtual list with useVirtualAxis."
url: "/packages/active-virtual/getting-started"
locale: "en"
revision: "8eb9b363747aac8e"
---

# Getting started

## Receive the package

`active-virtual` is an owner: request it on its own with `--owners active-virtual`, or it comes
along with `active-grid` or `active-geo`. Ask the maintainer of Foundation for a bundle, then follow
[Receiving packages](/receiving-packages):

```bash
node tools/rps/rps-apply.mjs plan --bundle <bundle-folder> --development
node tools/rps/rps-apply.mjs apply --bundle <bundle-folder> --expect-plan <digest> --development
```

## Declare, install and verify

```json
{
  "dependencies": {
    "@workspace/active-virtual": "workspace:*",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}
```

```bash
pnpm install
node tools/rps/rps-apply.mjs verify-install --from <app-folder>
```

## Render a list

```tsx
'use client';

import {useState} from 'react';
import {useVirtualAxis} from '@workspace/active-virtual/react';

export function VirtualList({rows}: {rows: string[]}) {
  const [container, setContainer] = useState<HTMLDivElement | null>(null);
  const {projection, itemRef} = useVirtualAxis({
    count: rows.length,
    estimate: 32,
    container,
  });
  return (
    <div ref={setContainer} style={{height: 400, overflow: 'auto'}}>
      <div style={{height: projection.extent, position: 'relative'}}>
        {projection.items.map((item) => (
          <div
            key={item.identity}
            ref={itemRef(item.identity)}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              transform: `translateY(${item.offset}px)`,
            }}
          >
            {rows[item.position]}
          </div>
        ))}
      </div>
    </div>
  );
}
```

- Read `projection` during render; it is current for this render's inputs.
- Keep an estimate function stable (module level or `useCallback`): a new function is a new input.
- The container is read after commit; `null` leaves the axis unattached.

**Example: Ten thousand rows of varying height**

Client: Runs entirely in your browser. Source: `examples/virtual/long-list.tsx`.

```tsx
// apps/docs/examples/virtual/long-list.tsx
'use client';

import {useState} from 'react';

import {Button} from '@workspace/active-ui/components/primitives/button';
import {Input} from '@workspace/active-ui/components/primitives/input';
import {Label} from '@workspace/active-ui/components/primitives/label';
import {useVirtualAxis} from '@workspace/active-virtual/react';

const COUNT = 10_000;

/** Every seventh row wraps to a second line: the axis measures real heights. */
function rowText(position: number): string {
  const base = `Row ${(position + 1).toLocaleString('en')}`;
  return position % 7 === 3
    ? `${base}: a longer note that wraps to a second line in a narrow column, so this row is taller than its estimate.`
    : base;
}

/** 10,000 rows of varying height; only the rows in view (plus overscan) are in the DOM. */
export default function LongList() {
  const [container, setContainer] = useState<HTMLDivElement | null>(null);
  const [target, setTarget] = useState('5000');
  const {projection, controller, itemRef} = useVirtualAxis({
    count: COUNT,
    estimate: 32,
    container,
  });
  return (
    <div className="flex flex-col gap-3">
      <form
        className="flex flex-wrap items-end gap-2"
        onSubmit={(event) => {
          event.preventDefault();
          const row = Number(target);
          if (Number.isInteger(row) && row >= 1 && row <= COUNT)
            controller.scrollToPosition(row - 1, {align: 'center'});
        }}
      >
        <div className="flex flex-col gap-1.5">
          <Label htmlFor="virtual-target">Row</Label>
          <Input
            id="virtual-target"
            className="w-28"
            inputMode="numeric"
            value={target}
            onChange={(event) => setTarget(event.target.value)}
          />
        </div>
        <Button type="submit" variant="outline">
          Go to row
        </Button>
      </form>
      <div
        ref={setContainer}
        className="bg-background h-72 overflow-auto rounded-md border"
        tabIndex={0}
        aria-label="Ten thousand rows"
        data-virtual-container
      >
        <div style={{height: projection.extent, position: 'relative'}}>
          {projection.items.map((item) => (
            <div
              key={item.identity}
              ref={itemRef(item.identity)}
              className="border-b px-3 py-1.5 text-sm"
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                transform: `translateY(${item.offset}px)`,
              }}
              data-position={item.position}
            >
              {rowText(item.position)}
            </div>
          ))}
        </div>
      </div>
      <p className="text-muted-foreground text-sm" aria-live="polite">
        {projection.items.length} of {COUNT.toLocaleString('en')} rows in the
        DOM
      </p>
    </div>
  );
}
```
