Skip to main content

React component

Megane can be embedded in React applications as a component library, published to npm as megane-viewer. It provides both high-level React components (MeganeViewer, PipelineViewer) and the framework-agnostic MoleculeRenderer core renderer.

The Megane renderer running live in this page — the same engine megane-viewer ships to your app.

Installation​

npm install megane-viewer

megane-viewer works with both React 18 and React 19 — it declares a peer dependency of react@^18.2.0 || ^19.0.0 (and the matching react-dom) and uses your app's React instance rather than bundling its own.

The package ships TypeScript declarations for both entry points (megane-viewer and megane-viewer/lib), so no manual ambient declarations are needed.

Import the bundled stylesheet once (e.g. in your app entry) so the viewer UI is styled:

import "megane-viewer/styles.css";

The easiest way to get started is the MeganeViewer component. It includes the 3D viewport, pipeline editor, timeline, tooltip, and measurement panels — everything you need in a single component. Every tool except the 3D viewport can be switched off through the ui prop.

import { useCallback } from "react";
import "megane-viewer/styles.css";
import { MeganeViewer } from "megane-viewer/lib";

function App() {
// The viewer has already loaded the file into its Load Structure node when
// this fires — it is a notification, not a request to load the file.
const handleUpload = useCallback((file: File) => {
console.log("opened", file.name);
}, []);

return (
<MeganeViewer
onUploadStructure={handleUpload}
width="100%"
height="600px"
/>
);
}

MeganeViewer Props​

MeganeViewer is pipeline-store-driven: it manages its own rendering state internally. Host apps supply only callbacks that are told about opened files; viewer state (snapshot, bonds, labels, vectors, etc.) is derived from the internal pipeline graph.

PropTypeRequiredDescription
onUploadStructure(file: File, preParsed?: StructureParseResult) => void✓Called after a file opened in the primary Load Structure node has been parsed and applied to the pipeline — do not call openFile from it, which would parse and apply the file a second time. preParsed carries the viewer's own parse result (the npm build always parses eagerly, so it is always set; the bundled web app may hand a large multi-frame XYZ / PDB file over unparsed for streaming). Pass () => {} when you have nothing to do
onUploadTrajectory(file: File) => voidCalled when the user opens a file in a Load Trajectory node
onFrameChange(frame: number) => voidFires whenever the active trajectory frame changes (scrub, playback tick, seek) — mirrors the Jupyter widget's frame_change event
onSelectionChange(selection: SelectionState) => voidFires whenever the atom selection changes; data { atoms: number[] } (0-based, max 4) — mirrors selection_change
onMeasurementChange(measurement: Measurement | null) => voidFires when the distance / angle / dihedral measurement changes, null when cleared — mirrors measurement
playingbooleanPlayback state (default: false). Used only together with onPlayPause; otherwise the viewer's own playback store drives the Timeline
fpsnumberPlayback speed (default: 30). Used only together with onFpsChange
onSeek(frame: number) => voidFrame seek handler (default: the internal playback store)
onPlayPause() => voidPlay/pause toggle — supplying it makes playback controlled by playing
onFpsChange(fps: number) => voidFPS change handler — supplying it makes the speed controlled by fps
initialCameraStateMeganeCameraState | nullCamera to restore on the first structure load; takes precedence over the built-in localStorage copy
onCameraStateChange(state: MeganeCameraState) => voidCalled after each camera interaction, for hosts that persist the camera in their own storage
width / heightstring | numberViewer dimensions (default: "100%")
uiPartial<MeganeViewerUiOptions>Hides individual tools — see below (default: everything visible)

onBondSourceChange, onLabelSourceChange, onLoadLabelFile, onVectorSourceChange, onLoadVectorFile and onLoadDemoVectors are deprecated: they are still accepted so existing code keeps compiling, but they are never called — bonds, labels and vectors are configured by the pipeline's nodes. Drop them from your props; they will be removed in a future release.

Hiding tools (ui)​

Every tool except the 3D viewport can be hidden at construction time. Omitted keys stay visible, so you only list what you want gone:

import { MeganeViewer, usePipelineStore } from "megane-viewer/lib";

// Read-only embed: 3D view and playback only.
<MeganeViewer
ui={{ pipelineEditor: false, resetView: false, perfHud: false }}
onUploadStructure={() => {}}
/>;

// With no editor there is no Load Structure node to open a file in,
// so load files yourself:
await usePipelineStore.getState().openFile(file);
KeyDefaultHides
pipelineEditortruePipeline editor panel on the right, including the viewer's only file-open UI
resetViewtrue"Reset View" button in the top-left corner (re-fits the structure in the standard orientation)
viewAxestrueAxis-alignment buttons under Reset View: ±a / ±b / ±c while a cell is loaded, ±x / ±y / ±z always
perfHudtrueAtoms / Bonds / Draws / FPS readout
timelinetruePlayback timeline along the bottom
tooltiptrueHover tooltip over atoms and bonds
measurementtrueMeasurement readout panel and saved-measurement list (selection itself stays active — press Esc to clear it)

Hiding a tool removes it from the DOM but changes nothing about the scene — the pipeline still executes and the renderer still receives every update. The type MeganeViewerUiOptions and the all-visible defaults DEFAULT_MEGANE_VIEWER_UI are exported for typed configs. With pipelineEditor: false you must drive file loading yourself (usePipelineStore.getState().openFile(file)), since the Load Structure node lives inside the editor.

Light / dark theme​

MeganeViewer injects its colour tokens itself, but its panels only switch to the dark palette while <html> carries data-theme="dark". To follow the OS (or VS Code / JupyterLab) theme, mount <ThemeSync /> once next to your app root — or call installThemeSync(), which returns an undo function — and it keeps that attribute in step with the theme store. useThemeStore.getState().setTheme("dark") (or "light", "system") forces one; the choice is saved in localStorage under megane-theme.

import { MeganeViewer, ThemeSync } from "megane-viewer/lib";

<>
<ThemeSync />
<MeganeViewer onUploadStructure={handleUpload} />
</>;

Camera orientation​

The initial view and "Reset View" use VESTA's standard orientation of crystal shape, derived from the structure's actual lattice vectors: +c points up the screen and +b to the right, then the eye is swung by arctan(1/3) ≈ 18.4° around the vertical axis and raised by arctan(1/6) ≈ 9.5°, so the +a end of the cell comes toward the viewer at the lower left. Structures without a cell use the Cartesian frame (a = x, b = y, c = z).

The axis buttons (ui.viewAxes) turn the camera to look straight along an axis from its + or − side while keeping the target, distance and zoom; the same operation is available programmatically:

import { MoleculeRenderer, standardOrientation, axisOrientation } from "megane-viewer/lib";

renderer.alignCameraToAxis("+c"); // camera above the cell, b up
renderer.resetView(); // standard orientation, re-fitted

// Pure helpers, no renderer needed: unit `eye` (target → camera) and `up`.
standardOrientation(snapshot.box); // VESTA standard orientation
axisOrientation("-b", snapshot.box); // camera on the −b side, c up

Mouse controls​

The camera follows OVITO's orbit mode on every host:

InputAction
Left dragOrbit around the rotation center. Horizontal motion spins about +c (+z without a cell), vertical motion tilts; the model never rolls, and the tilt keeps going over the poles instead of stopping there
Right drag, or Shift + left dragPan
Middle drag, wheelZoom
Double-click an atom or bondMake it the rotation center (marked by a 3D cross). In the default orthographic projection the view does not move; with perspective on, the point is panned to the center
Double-click empty spaceReset the rotation center to the model center

A periodic image counts as the atom you see: double-clicking it pivots on that copy, not on the original in the home cell. Atoms hidden with a Modify node's opacity: 0 cannot be picked, so they never steal a double-click, hover or selection from the atoms behind them. renderer.setRotationCenter(x, y, z) and renderer.resetRotationCenter() expose the same operations.

See the TypeScript Pipeline API for the complete interface.


Multiple viewers on one page​

By default MeganeViewer reads a set of module-global stores. That is what a single-viewer app wants, and it is why the snippet above can reach the viewer's state through usePipelineStore.getState() from anywhere. But it also means two viewers mounted side by side share one pipeline: the second one's openFile() replaces the graph the first is rendering, and both panels end up showing the same structure.

Wrap each viewer in a MeganeProvider and it gets its own state:

import { useEffect, useState } from "react";
import "megane-viewer/styles.css";
import {
MeganeViewer,
MeganeProvider,
createMeganeStores,
type MeganeStores,
} from "megane-viewer/lib";

function Preview({ stores, text, fileName }: {
stores: MeganeStores;
text: string;
fileName: string;
}) {
useEffect(() => {
// Straight onto THIS viewer's pipeline store.
void stores.pipeline.getState().openFile(new File([text], fileName));
}, [stores, text, fileName]);

return (
<MeganeProvider stores={stores}>
<MeganeViewer onUploadStructure={() => {}} width="100%" height="100%" />
</MeganeProvider>
);
}

function App() {
// Created once — not inside render, or every render starts a new viewer.
const [left] = useState(() => createMeganeStores());
const [right] = useState(() => createMeganeStores());

return (
<div style={{ display: "flex", height: 500 }}>
<Preview stores={left} text={crambinPdb} fileName="crambin.pdb" />
<Preview stores={right} text={butanePdb} fileName="butane.pdb" />
</div>
);
}

MeganeProvider can also create and own its bundle, which is all you need when nothing outside the viewer has to drive it:

<MeganeProvider>
<MeganeViewer onUploadStructure={handleUpload} />
</MeganeProvider>

What a bundle contains​

createMeganeStores() returns the six stores that belong to one viewer — pipeline, playback, measurement, viewState, pipelineUI and inspector — plus the file-drop handlers its Load nodes route through.

Three stores stay page-level and are deliberately not scoped:

StoreWhy it stays global
useThemeStoreWrites data-theme on <html>, and the CSS tokens are defined on :root. Two viewers scoping it would fight over one DOM node.
useAIConfigStoreOne API key and model per user.
useTourStoreOne guided-tour overlay per document.

Reaching a viewer's state​

Inside the provider's subtree, the scoped hooks mirror the global ones:

import { useScopedPipelineStore, usePipelineStoreApi } from "megane-viewer/lib";

function AtomCount() {
// Selector form, for the render path.
return <>{useScopedPipelineStore((s) => s.viewportState.particles[0]?.source?.nAtoms ?? 0)}</>;
}

function ToggleCellAxes() {
// Store API, for effects and event handlers. Writing viewportState directly
// updates the renderer without re-executing the graph, so the camera stays
// where the user left it.
const api = usePipelineStoreApi();
return <button onClick={() => {
const vs = api.getState().viewportState;
api.setState({ viewportState: { ...vs, cellAxesVisible: !vs.cellAxesVisible } });
}}>Toggle cell axes</button>;
}

Outside the subtree, hold the bundle you created and use stores.pipeline, stores.playback, and so on, as in the two-viewer example above.

Persistence​

A scoped bundle keeps its camera and panel tab in memory. The single-viewer defaults persist them under one fixed key each (megane-view-state in localStorage, megane-pipeline-ui in sessionStorage), which two viewers would overwrite for each other. Opt in per viewer with a key of your own:

createMeganeStores({
id: "left",
persist: { camera: "megane-view-state-left", pipelineUI: "megane-pipeline-ui-left" },
});

Compatibility​

Mounting no provider changes nothing: every hook falls back to the module-global stores, so usePipelineStore.getState().openFile(file) and the rest of this page keep working as before.


PipelineViewer (Docs / MDX Embed)​

PipelineViewer is a self-contained React component designed for embedding molecular visualizations in documentation, blog posts, and MDX pages. It uses no shared stores — each instance creates its own private viewport and playback stores, so multiple instances on the same page are independent with no extra wiring. MeganeViewer can also be mounted more than once per page, by giving each one its own store bundle — see Multiple viewers on one page.

Key differences from MeganeViewer​

FeatureMeganeViewerPipelineViewer
Editing UI (pipeline editor, measurement panels, perf HUD)YesNo
Pipeline controlInternal editor UIpipeline prop
Multiple instances per pageYes, with MeganeProviderFully independent
File loadingUpload / drag-dropURL fetch via fileUrl
Trajectory playbackYesYes (Timeline shown automatically)

Installation​

npm install megane-viewer

Usage​

import { PipelineViewer } from "megane-viewer/lib";

<PipelineViewer
width="100%"
height={500}
pipeline={{
version: 3,
nodes: [
{
id: "s1",
type: "load_structure",
fileName: "caffeine_water.pdb",
fileUrl: "/structures/caffeine_water.pdb",
hasTrajectory: false,
hasCell: false,
position: { x: 0, y: 0 },
},
{
id: "b1",
type: "add_bond",
bondSource: "distance",
position: { x: 200, y: 0 },
},
{
id: "v1",
type: "viewport",
perspective: false,
cellAxesVisible: true,
pivotMarkerVisible: true,
position: { x: 400, y: 0 },
},
],
edges: [
{ source: "s1", target: "b1", sourceHandle: "particle", targetHandle: "particle" },
{ source: "s1", target: "v1", sourceHandle: "particle", targetHandle: "particle" },
{ source: "b1", target: "v1", sourceHandle: "bond", targetHandle: "bond" },
],
}}
/>

Props​

PropTypeDefaultDescription
pipelineSerializedPipeline(required)Pipeline JSON describing the node graph
widthstring | number"100%"Component width
heightstring | number500Component height in pixels

SerializedPipeline format​

import type { PipelineNodeParams, SerializedPipeline } from "megane-viewer/lib";

// SerializedPipeline (exported from megane-viewer/lib):
interface SerializedPipeline {
version: 3;
nodes: Array<PipelineNodeParams & {
id: string;
position: { x: number; y: number };
enabled?: boolean; // false = node is bypassed (default: true)
}>;
edges: Array<{
source: string;
target: string;
sourceHandle: string;
targetHandle: string;
}>;
}

PipelineNodeParams is a discriminated union exported by megane-viewer/lib. The type field on each node determines which parameters are required — see Node Reference for the full list.

load_structure node — the fileUrl field​

PipelineViewer cannot use a local file picker, so load_structure nodes need a fileUrl field pointing to a URL where the structure file can be fetched:

{
id: "s1",
type: "load_structure",
fileName: "protein.pdb", // picks the parser by extension; null → the URL's last segment
fileUrl: "/structures/protein.pdb", // fetched at render time
hasTrajectory: false,
hasCell: false,
position: { x: 0, y: 0 },
}

The component fetches all fileUrl values in parallel using the browser's fetch() API, then parses them with the WASM parser. It does this whenever the pipeline prop changes identity, resetting the view, so memoize the pipeline (or define it outside the component) rather than passing a new object literal on every render.

Trajectory playback​

When the pipeline includes time-dependent data — by loading a multi-frame structure file (such as a multi-frame XYZ or ASE .traj) — PipelineViewer automatically shows the Timeline bar at the bottom.

<PipelineViewer
height={500}
pipeline={{
version: 3,
nodes: [
{
id: "s1",
type: "load_structure",
fileName: "simulation.traj",
fileUrl: "/structures/simulation.traj",
hasTrajectory: true,
hasCell: false,
position: { x: 0, y: 0 },
},
{
id: "v1",
type: "viewport",
perspective: false,
cellAxesVisible: false,
pivotMarkerVisible: true,
position: { x: 300, y: 0 },
},
],
edges: [
{ source: "s1", target: "v1", sourceHandle: "particle", targetHandle: "particle" },
{ source: "s1", target: "v1", sourceHandle: "trajectory", targetHandle: "trajectory" },
],
}}
/>

Note: PipelineViewer only fetches load_structure nodes, so load_trajectory nodes draw nothing. Trajectories must be embedded in the structure file (e.g. ASE .traj, multi-frame XYZ). Separate trajectory files (XTC, DCD, LAMMPS dump, …) need a MeganeViewer, where the user opens them in a Load Trajectory node.

Usage in MDX (Next.js / Docusaurus)​

PipelineViewer works in any MDX-based framework — import it directly in your .mdx file and drop it in. For full MDX examples, the MeganeViewer / viewport-only variants, and the required next.config.mjs WASM setup, see the MDX / Next.js guide.

Using a saved pipeline JSON​

You can serialize a pipeline from the Megane UI (Pipeline editor → File → Export pipeline, which downloads pipeline.megane.json) and load it directly:

import pipelineJson from "./my-pipeline.json";
import { PipelineViewer } from "megane-viewer/lib";

// Add fileUrl to each load_structure node before rendering
const pipeline = {
...pipelineJson,
nodes: pipelineJson.nodes.map((n) =>
n.type === "load_structure"
? { ...n, fileUrl: `/structures/${n.fileName}` }
: n,
),
};

<PipelineViewer pipeline={pipeline} height={500} />

Individual Components​

For custom layouts, Megane exports each panel as a separate component. This gives you full control over placement and behavior.

Viewport — 3D Canvas Only​

The core rendering surface without any UI panels:

import { Viewport } from "megane-viewer/lib";
import type { Snapshot, HoverInfo } from "megane-viewer/lib";

function MinimalViewer({ snapshot }: { snapshot: Snapshot }) {
return (
<Viewport
snapshot={snapshot}
frame={null}
onRendererReady={(renderer) => {
renderer.setAtomScale(1.5);
}}
onHover={(info: HoverInfo) => {
if (info?.kind === "atom") {
console.log(`Atom ${info.atomIndex}: ${info.elementSymbol}`);
}
}}
onAtomRightClick={(atomIndex) => {
console.log("Selected:", atomIndex);
}}
/>
);
}

Combine individual panels for a custom layout:

import { useState } from "react";
import { Viewport, Sidebar, MoleculeRenderer } from "megane-viewer/lib";
import type { BondConfig, TrajectoryConfig } from "megane-viewer/lib";

function CustomLayout({ bondConfig, trajectoryConfig, handleUpload }) {
const [renderer, setRenderer] = useState<MoleculeRenderer | null>(null);
const [collapsed, setCollapsed] = useState(false);

return (
<div style={{ position: "relative", width: "100%", height: "100vh" }}>
{/* Left sidebar */}
<Sidebar
mode="local"
structure={{ atomCount: 0, fileName: null }}
bonds={bondConfig}
trajectory={trajectoryConfig}
onUploadStructure={handleUpload}
onResetView={() => renderer?.resetView()}
hasCell={false}
cellVisible={false}
onToggleCell={() => {}}
collapsed={collapsed}
onToggleCollapse={() => setCollapsed((c) => !c)}
/>

{/* 3D viewport */}
<Viewport
snapshot={null}
frame={null}
onRendererReady={setRenderer}
/>
</div>
);
}

Timeline is exported too. It is fully controlled: pass the current frame, frame count, playback, speed and loop state, and a handler for each control.


Core Renderer (Vue / Svelte / vanilla JS)​

For non-React applications (Vue, Svelte, vanilla JS), use MoleculeRenderer directly — the same Three.js renderer that powers all Megane components. See the guide to using Megane with Vue, Svelte & vanilla JS for mounting, appearance control, atom selection/measurement, and screen-space picking.


MDX Usage (Next.js)​

Megane works in MDX-based documentation frameworks like Next.js. For static embeds, MeganeViewer/PipelineViewer variants, viewport-only embeds, and the required next.config.mjs WASM setup, see the MDX / Next.js guide.


Protocol Utilities​

Decode binary messages from the megane serve backend (WebSocket):

import {
decodeSnapshot,
decodeFrame,
decodeMetadata,
decodeHeader,
MSG_SNAPSHOT,
MSG_FRAME,
MSG_METADATA,
} from "megane-viewer/lib";

ws.onmessage = (event) => {
const buffer = event.data as ArrayBuffer;
const header = decodeHeader(buffer);

switch (header.msgType) {
case MSG_SNAPSHOT:
const snapshot = decodeSnapshot(buffer);
renderer.loadSnapshot(snapshot);
break;
case MSG_FRAME:
const frame = decodeFrame(buffer);
renderer.updateFrame(frame);
break;
case MSG_METADATA:
const meta = decodeMetadata(buffer);
console.log(`${meta.nFrames} frames, ${meta.timestepPs} ps/step`);
break;
}
};

Types​

Key TypeScript types exported by Megane:

import type {
Snapshot, // Parsed molecular structure (positions, elements, bonds)
Frame, // Single trajectory frame (frameId, positions)
TrajectoryMeta, // Trajectory metadata (nFrames, timestepPs)
HoverInfo, // Atom/bond hover information
SelectionState, // Current atom selection
Measurement, // Distance/angle/dihedral result
BondSource, // "structure" | "file" | "distance" | "none"
BondConfig, // Bond panel configuration (for Sidebar)
TrajectoryConfig, // Trajectory panel configuration (for Sidebar)
StructureParseResult,// Parse result from parseStructureFile/Text
MeganeCameraState, // Camera state for initialCameraState / onCameraStateChange
} from "megane-viewer/lib";

Parser Functions​

import { parseStructureFile, parseStructureText } from "megane-viewer/lib";

// Parse from File object (drag-and-drop, file input)
const result = await parseStructureFile(file);
// result.snapshot, result.frames, result.meta, result.labels,
// result.vectorChannels, result.scalarChannels, result.warnings

// Parse from text string (fetched content). The optional file name picks
// the format by extension; without it the text is parsed as PDB.
const fromText = await parseStructureText(groText, "conf.gro");