# Getting started

URL: https://schedgrid.fabio.cv/docs/getting-started

Install schedgrid and render a first grid, with or without React.

## Install [#install]

```sh
pnpm add schedgrid
```

The package is ESM only and ships four sub-path exports. React is an
optional peer dependency: install it only if you use `schedgrid/react`.

## Without a framework [#without-a-framework]

The engine needs two things from the outside world: the size of the
viewport and the current scroll offset. Everything else is derived.

```ts
import { createGrid } from 'schedgrid/core';
import { bindScrollElement } from 'schedgrid/dom';

const grid = createGrid({
  rows: { count: 200, size: 40, pinned: 1 },
  columns: { count: 97, size: 24, pinned: 1 },
  items: [
    { id: 'a', rows: [1, 2], columns: [30, 38] },
    { id: 'b', rows: [1, 2], columns: [34, 41] }, // overlaps `a`: gets a lane
  ],
});

const scroller = document.querySelector<HTMLElement>('#scroller')!;
const unbind = bindScrollElement(grid, scroller);

grid.subscribe(() => {
  const frame = grid.getFrame();
  // frame.rows, frame.columns and frame.items are plain numbers in
  // content space; paint them however you like.
});
```

`bindScrollElement` reads the element's size up front, moves it to the grid's
scroll offset, and keeps the two in sync from then on in both directions. Call
`unbind()` when the element goes away.

## With React [#with-react]

The hooks wrap the same engine. `useGrid` creates it once per component,
`useFrame` subscribes to it, and `useScrollContainer` returns a callback ref
for the scrollable element.

```tsx
import { useFrame, useGrid, useScrollContainer } from 'schedgrid/react';
import type { Item } from 'schedgrid/core';

const items: Item[] = [
  { id: 'a', rows: [1, 2], columns: [30, 38] },
  { id: 'b', rows: [1, 2], columns: [34, 41] }, // overlaps `a`: gets a lane
];

export function Schedule() {
  const grid = useGrid({
    rows: { count: 200, size: 40, pinned: 1 },
    columns: { count: 97, size: 24, pinned: 1 },
    items,
  });
  const frame = useFrame(grid);
  const ref = useScrollContainer(grid);

  return (
    <div ref={ref} style={{ height: 400, overflow: 'auto' }}>
      <div
        style={{
          position: 'relative',
          width: frame.total.width,
          height: frame.total.height,
        }}
      >
        {frame.items.map((it) => (
          <div
            key={it.id}
            style={{
              position: 'absolute',
              transform: `translate(${it.x}px, ${it.y}px)`,
              width: it.width,
              height: it.height,
            }}
          >
            {it.id}
          </div>
        ))}
      </div>
    </div>
  );
}
```

The frame object is memoised by the engine, so React bails out of
re-renders whenever nothing relevant changed. See the
[React page](/docs/react) for a complete renderer with pinned rows and
columns.

## Mapping time onto columns [#mapping-time-onto-columns]

Items live in index space. `schedgrid/time` turns a time range into an axis
and turns moments into fractional indices:

```ts
import { createGrid } from 'schedgrid/core';
import { timeAxis } from 'schedgrid/time';

const day = timeAxis({
  start: new Date(2026, 8, 7),
  end: new Date(2026, 8, 8),
  slot: 15 * 60_000, // one column per 15 minutes
  size: 24,
});

const grid = createGrid({
  rows: { count: 50, size: 40 },
  columns: day.axis,
  items: [
    {
      id: 'standup',
      rows: [0, 1],
      columns: day.toRange(
        new Date(2026, 8, 7, 9, 37),
        new Date(2026, 8, 7, 10, 30),
      ), // [38.47, 42]
    },
  ],
});
```