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.
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";
Full-Featured Viewer
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.
| Prop | Type | Required | Description |
|---|---|---|---|
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) => void | Called when the user opens a file in a Load Trajectory node | |
onFrameChange | (frame: number) => void | Fires whenever the active trajectory frame changes (scrub, playback tick, seek) — mirrors the Jupyter widget's frame_change event | |
onSelectionChange | (selection: SelectionState) => void | Fires whenever the atom selection changes; data { atoms: number[] } (0-based, max 4) — mirrors selection_change | |
onMeasurementChange | (measurement: Measurement | null) => void | Fires when the distance / angle / dihedral measurement changes, null when cleared — mirrors measurement | |
playing | boolean | Playback state (default: false). Used only together with onPlayPause; otherwise the viewer's own playback store drives the Timeline | |
fps | number | Playback speed (default: 30). Used only together with onFpsChange | |
onSeek | (frame: number) => void | Frame seek handler (default: the internal playback store) | |
onPlayPause | () => void | Play/pause toggle — supplying it makes playback controlled by playing | |
onFpsChange | (fps: number) => void | FPS change handler — supplying it makes the speed controlled by fps | |
initialCameraState | MeganeCameraState | null | Camera to restore on the first structure load; takes precedence over the built-in localStorage copy | |
onCameraStateChange | (state: MeganeCameraState) => void | Called after each camera interaction, for hosts that persist the camera in their own storage | |
width / height | string | number | Viewer dimensions (default: "100%") | |
ui | Partial<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);
| Key | Default | Hides |
|---|---|---|
pipelineEditor | true | Pipeline editor panel on the right, including the viewer's only file-open UI |
resetView | true | "Reset View" button in the top-left corner (re-fits the structure in the standard orientation) |
viewAxes | true | Axis-alignment buttons under Reset View: ±a / ±b / ±c while a cell is loaded, ±x / ±y / ±z always |
perfHud | true | Atoms / Bonds / Draws / FPS readout |
timeline | true | Playback timeline along the bottom |
tooltip | true | Hover tooltip over atoms and bonds |
measurement | true | Measurement 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:
| Input | Action |
|---|---|
| Left drag | Orbit 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 drag | Pan |
| Middle drag, wheel | Zoom |
| Double-click an atom or bond | Make 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 space | Reset 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:
| Store | Why it stays global |
|---|---|
useThemeStore | Writes data-theme on <html>, and the CSS tokens are defined on :root. Two viewers scoping it would fight over one DOM node. |
useAIConfigStore | One API key and model per user. |
useTourStore | One 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
| Feature | MeganeViewer | PipelineViewer |
|---|---|---|
| Editing UI (pipeline editor, measurement panels, perf HUD) | Yes | No |
| Pipeline control | Internal editor UI | pipeline prop |
| Multiple instances per page | Yes, with MeganeProvider | Fully independent |
| File loading | Upload / drag-drop | URL fetch via fileUrl |
| Trajectory playback | Yes | Yes (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
| Prop | Type | Default | Description |
|---|---|---|---|
pipeline | SerializedPipeline | (required) | Pipeline JSON describing the node graph |
width | string | number | "100%" | Component width |
height | string | number | 500 | Component 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:
PipelineVieweronly fetchesload_structurenodes, soload_trajectorynodes 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 aMeganeViewer, 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);
}}
/>
);
}
Sidebar
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");