Skip to content

Composed multi-pane Reader ​

This page has not been reviewed by a human.

You are building a reading app. The complete <sefaria-reader> shows one reading surface. Your readers need more. They want to keep an earlier passage in view, open a connected passage next to it, and see that passage's connections as well.

It is an advanced example, not the recommended first path. Start with Add the complete Reader unless you need this layout.

On wide screens the panes sit side by side and scroll on their own. Selecting a connection opens a new source pane and a connections pane. A close button removes a pane and the panes opened from it. You can't close the first pane.

On narrow screens, path buttons choose the visible pane. Returning to an earlier passage removes the panes opened after that passage.

It is a complete app (Vite and TypeScript) in examples/reader. You can't edit it on this page. Read the source on GitHub.

Open in a new tab

The demo makes no Sefaria request until you choose "Start live demo". The reference field starts as Micah 6:8. A ?tref= query in the address can fill it in for you.

How it works ​

The example uses a Reader session. You create one with createReaderSession from @sefaria/web-components/reader-session. A session is plain data and functions, with no elements. It keeps the reading history, selected passage, and captured source and connections records. It also keeps each connections pane's category and page, and holds pins that keep the session from discarding entries.

A session doesn't make requests and has no events. You fetch the data yourself and hand it to the session. Each change returns a new session value that you keep. The complete Reader wraps a session in a controller that also handles loading.

The host page, app.ts, owns everything else. That covers:

  • the layout and CSS
  • which panes exist and which is active
  • close buttons and the form
  • status and errors
  • cancelling requests
  • wiring events

The host page pins the entries of visible panes and releases them when panes close.

Start a session ​

The host resolves the reference with a client-backed data source and creates a session from the result.

ts
const destination = await resolveReaderSource(
  { tref: targetRef.trim() },
  dataSource,
  currentController.signal,
);
if (
  currentController.signal.aborted ||
  currentGeneration !== generation
) {
  return;
}
releasePanes(spatial?.panes ?? []);
session = createReaderSession({
  source: {
    record: destination.record,
    ...(destination.selectedPosition === undefined
      ? {}
      : { selectedPosition: destination.selectedPosition }),
  },
  presentation: {
    vocalizationMode: readVocalizationMode(vocalizationMode.value),
  },
});

Feed a Source Card ​

A Source Card gets its data from the session's captured record, so it makes no request of its own. Its sefaria-source-select event goes back to the host. Show an attributed passage covers the Source Card on its own.

ts
const source = document.createElement(
  "sefaria-source-card",
) as SefariaSourceCard;
source.data = record.payload;
source.selectedPosition = entry.selectedPosition;
source.setAttribute("content-language", entry.presentation.contentLanguage);
source.setAttribute("layout", entry.presentation.layout);
source.setAttribute("side-order", entry.presentation.sideOrder);
source.setAttribute(
  "vocalization-mode",
  entry.presentation.vocalizationMode,
);
source.setAttribute("selectable", "");
source.addEventListener("sefaria-source-select", (event) => {
  const detail = (
    event as CustomEvent<{
      readonly position: readonly number[];
      readonly ref: string;
    }>
  ).detail;
  void selectSource(pane, detail.position, detail.ref);
});

Wire the connections ​

The Connections Panel (see Show commentary and connected texts) sends its category, page and connection-select events to the host and the session.

ts
connections.addEventListener(
  "sefaria-connections-category-change",
  (event) => {
    const category = (
      event as CustomEvent<{ readonly category: string | null }>
    ).detail.category;
    projectConnections(pane, category === null ? {} : { category });
  },
);
connections.addEventListener("sefaria-connections-page-change", (event) => {
  const page = (event as CustomEvent<{ readonly page: number }>).detail
    .page;
  const category = session?.connectionsRecord(entry.entryId)?.projection
    .category;
  projectConnections(pane, {
    ...(category === undefined ? {} : { category }),
    page,
  });
});
connections.addEventListener("sefaria-connection-select", (event) => {
  const targetRef = (event as CustomEvent<{ readonly targetRef: string }>)
    .detail.targetRef;
  void openConnectedSource(pane, targetRef).catch((error: unknown) => {
    status.textContent = `${targetRef} could not be opened.`;
    showError(error);
  });
});

The example listens directly to sefaria-connections-category-change, sefaria-connections-page-change and sefaria-connection-select.

Requests stay low ​

Children receive data from captured records, so they don't fetch (see How the toolkit works). Opening a passage makes a text request and a links request. A connected passage you open adds its own text and links requests. It sometimes adds one more text request for its surrounding section.

Changing the category or page makes no request, because the panel reuses the links it already has. Choosing a different verse in an earlier pane closes the panes opened from it and makes one links request (with the cache off).

Run it locally ​

You need Node.js 22.12 or later and the pnpm version pinned in package.json. From the repository root, run pnpm install, then pnpm build, then pnpm dev:reader.

When to use something else ​

If one integrated reading surface with built-in history is enough, use the complete Reader. For that, see Add the complete Reader. The repository also has a standalone Reader demo. Component details are in the Components reference.

Sefaria Frontend ToolkitCommunity-driven with Sefaria backing and support. GitHub Get support

AI-generated documentation.

About this documentation

Like the codebase, this documentation was scaffolded and written with GitHub Copilot. I don't have a background in technical writing, and wanted to get something out there.

Pages that have not undergone any human review are explicitly marked, but the others may also read funny or need work too. Sorry about that.

Please report any issues so they can be prioritized.

Thanks, Avi