Skip to main content

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

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.

PropTypeRequiredDescription
onUploadStructure(file: File) => voidCalled when the user uploads a structure file
onUploadTrajectory(file: File) => voidCalled when the user uploads a trajectory file
onBondSourceChange(source: BondSource) => voidBond source change callback
onLabelSourceChange(source: LabelSource) => voidLabel source change callback
onLoadLabelFile(file: File) => voidCalled when the user uploads a label file
onVectorSourceChange(source: VectorSource) => voidVector source change callback
onLoadVectorFile(file: File) => voidCalled when the user uploads a vector file
onLoadDemoVectors() => voidCalled when the user requests demo vectors
playingbooleanPlayback state (default: false)
fpsnumberPlayback speed (default: 30)
onSeek(frame: number) => voidFrame seek handler
onPlayPause() => voidPlay/pause toggle
onFpsChange(fps: number) => voidFPS change handler
width / heightstring | numberViewer 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

FeatureMeganeViewerPipelineViewer
UI panels (sidebar, appearance)YesNo
Pipeline controlInternal editor UIpipeline prop
Multiple instances per pageConflicts (global store)Fully 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.

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: PipelineViewer does not currently support load_trajectory nodes. Trajectories must be embedded in the structure file (e.g. ASE .traj, multi-frame XYZ). External XTC trajectories require a MeganeViewer with 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);
}}
/>
);
}

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);