Web / React
megane can be embedded in React applications as a component library. It provides both high-level React components and a framework-agnostic imperative renderer.
Installation
npm install megane-viewer
Full-Featured Viewer
The easiest way to get started is the MeganeViewer component. It includes the 3D viewport, sidebar, appearance panel, timeline, tooltip, and measurement panel — everything you need in a single component.
import { useCallback } from "react";
import { MeganeViewer } from "megane-viewer/lib";
import { usePipelineStore } from "megane-viewer/lib";
function App() {
const handleUpload = useCallback((file: File) => {
usePipelineStore.getState().openFile(file);
}, []);
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 the file-ingestion callbacks; viewer state (snapshot, bonds, labels, vectors, etc.) is derived from the internal pipeline graph.
| Prop | Type | Required | Description |
|---|---|---|---|
onUploadStructure | (file: File) => void | ✓ | Called when the user uploads a structure file |
onUploadTrajectory | (file: File) => void | Called when the user uploads a trajectory file | |
onBondSourceChange | (source: BondSource) => void | Bond source change callback | |
onLabelSourceChange | (source: LabelSource) => void | Label source change callback | |
onLoadLabelFile | (file: File) => void | Called when the user uploads a label file | |
onVectorSourceChange | (source: VectorSource) => void | Vector source change callback | |
onLoadVectorFile | (file: File) => void | Called when the user uploads a vector file | |
onLoadDemoVectors | () => void | Called when the user requests demo vectors | |
playing | boolean | Playback state (default: false) | |
fps | number | Playback speed (default: 30) | |
onSeek | (frame: number) => void | Frame seek handler | |
onPlayPause | () => void | Play/pause toggle | |
onFpsChange | (fps: number) => void | FPS change handler | |
width / height | string | number | Viewer dimensions (default: "100%") |
See the TypeScript Pipeline API for the complete interface.
PipelineViewer (Docs / MDX Embed)
PipelineViewer is a self-contained React component designed for embedding molecular visualizations in documentation, blog posts, and MDX pages. Unlike MeganeViewer, it has no dependency on global stores — multiple instances on the same page are fully independent and each manages its own playback state.
Key differences from MeganeViewer
| Feature | MeganeViewer | PipelineViewer |
|---|---|---|
| UI panels (sidebar, appearance) | Yes | No |
| Pipeline control | Internal editor UI | pipeline prop |
| Multiple instances per page | Conflicts (global store) | 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.
Each node's type field 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", // displayed name (optional, inferred from URL)
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 at mount time using the browser's fetch() API, then parses them with the WASM parser.
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:
PipelineViewerdoes not currently supportload_trajectorynodes. Trajectories must be embedded in the structure file (e.g. ASE.traj, multi-frame XYZ). External XTC trajectories require aMeganeViewerwith a server-side pipeline.
Usage in MDX (Next.js / Docusaurus)
PipelineViewer works in any MDX-based framework. Import it directly in your .mdx file:
import { PipelineViewer } from "megane-viewer/lib";
# Caffeine in Water
<PipelineViewer
height={480}
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: false,
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" },
],
}}
/>
For Next.js you also need WASM support in next.config.mjs:
const nextConfig = {
webpack: (config) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};
export default nextConfig;
Using a saved pipeline JSON
You can serialize a pipeline from the megane UI (Pipeline editor → Export) 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, Timeline
Combine individual panels for a custom layout:
import { useState } from "react";
import { Viewport, Sidebar, Timeline } from "megane-viewer/lib";
import type { BondConfig, TrajectoryConfig } from "megane-viewer/lib";
function CustomLayout({ bondConfig, trajectoryConfig, handleUpload }) {
const [renderer, setRenderer] = useState(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>
);
}
Core Renderer (Framework-Agnostic)
For non-React applications (Vue, Svelte, vanilla JS), use MoleculeRenderer directly. This is the same Three.js renderer powering all megane components:
import { MoleculeRenderer } from "megane-viewer/lib";
import type { Snapshot } from "megane-viewer/lib";
// Create and mount
const renderer = new MoleculeRenderer();
renderer.mount(document.getElementById("viewer")!);
// Load data
renderer.loadSnapshot(snapshot);
// Update positions for animation
renderer.updateFrame(frame);
// Control appearance
renderer.setAtomScale(1.2);
renderer.setAtomOpacity(0.8);
renderer.setBondScale(0.5);
renderer.setPerspective(true);
renderer.setCellVisible(true);
// Cleanup
renderer.dispose();
Atom Selection & Measurement
// Select atoms programmatically
renderer.toggleAtomSelection(0);
renderer.toggleAtomSelection(1);
// Get measurement (distance, angle, or dihedral based on # selected)
const measurement = renderer.getMeasurement();
// { type: 'distance', value: 3.82, label: '3.82 Å', atoms: [0, 1] }
// Clear selection
renderer.clearSelection();
Picking (Hover & Click)
Screen-space picking for identifying atoms and bonds:
const hit = renderer.raycastAtPixel(mouseX, mouseY);
if (hit) {
if (hit.kind === "atom") {
console.log(`Atom ${hit.atomIndex}: ${hit.elementSymbol}`);
} else if (hit.kind === "bond") {
console.log(`Bond: ${hit.atomA}–${hit.atomB}, length=${hit.bondLength.toFixed(2)} Å`);
}
}
MDX Usage (Next.js)
megane works in MDX-based documentation frameworks like Next.js. For a static
embed of a known structure, use PipelineViewer
— it has no global-store dependency, so multiple instances on the same page
work independently and you don't need any file-upload plumbing:
---
title: Protein Visualization
---
import { PipelineViewer } from "megane-viewer/lib";
# Caffeine in Water
A caffeine molecule solvated by water — 3024 atoms rendered in real time.
<PipelineViewer
height={500}
pipeline={{
version: 3,
nodes: [
{ id: "s1", type: "load_structure", fileName: "caffeine_water.pdb",
fileUrl: "/data/protein.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: false,
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" },
],
}}
/>
The viewer above uses WASM-powered parsing and billboard impostor rendering
to display all 3024 atoms with bonds inferred from van der Waals radii.
If you specifically need the full sidebar / appearance / pipeline-editor UI in
an MDX page, use MeganeViewer and pipe uploads through usePipelineStore:
import { useCallback } from "react";
import { MeganeViewer, usePipelineStore } from "megane-viewer/lib";
export function FullViewer() {
const handleUpload = useCallback((file) => {
usePipelineStore.getState().openFile(file);
}, []);
return (
<MeganeViewer
onUploadStructure={handleUpload}
width="100%"
height="500px"
/>
);
}
<FullViewer />
Note that MeganeViewer is backed by a global Zustand store, so only one
instance per page renders correctly — for multiple independent viewers, use
PipelineViewer as shown above.
Viewport-Only in MDX
For a simpler embed without panels:
import { useState, useEffect } from "react";
import { Viewport, parseStructureText } from "megane-viewer/lib";
export function SimpleViewer() {
const [snapshot, setSnapshot] = useState(null);
useEffect(() => {
fetch("/data/caffeine_water.pdb")
.then((r) => r.text())
.then(async (text) => {
const result = await parseStructureText(text);
setSnapshot(result.snapshot);
});
}, []);
return (
<div style={{ height: "400px" }}>
<Viewport snapshot={snapshot} frame={null} />
</div>
);
}
<SimpleViewer />
Next.js Configuration
Add megane to next.config.mjs for WASM support:
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};
export default nextConfig;
Protocol Utilities
Decode binary messages from the megane WebSocket server:
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
} 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.labels
// Parse from text string (fetched content)
const result = await parseStructureText(pdbText);