Schematex

Interactive editing

Embed the open-source React or Vanilla Schematex editor, understand its complete API, and persist safe DSL edits.

Schematex 1.0 ships the editor in the same repository and npm package as the renderer. Every canvas operation produces ordinary DSL: there is no private document model and no second JSON format to synchronize.

Install and runtime

npm install schematex

InteractiveSchematexDiagram requires schematex >= 1.0.0; the built-in viewport requires schematex >= 1.0.1. Both support React 18 or newer. React and React DOM are optional peer dependencies because non-React users can continue using the core renderer without installing them.

No Schematex stylesheet import is required. The component injects its minimal selection, cursor, and label-editor styles; use className, canvasClassName, and labelEditorStyle for application-specific layout and appearance.

React quick start

'use client';

import { useState } from 'react';
import { InteractiveSchematexDiagram } from 'schematex/react';

export function Editor({ initialDsl }: { initialDsl: string }) {
  const [dsl, setDsl] = useState(initialDsl);

  return (
    <InteractiveSchematexDiagram
      value={dsl}
      onChange={(nextDsl, { reason, item }) => {
        setDsl(nextDsl);
        // Save nextDsl or push it into your own undo/collaboration model.
        console.log(reason, item?.semanticId);
      }}
      onSelect={(item) => console.log(item?.key)}
      ariaLabel="Architecture diagram editor"
    />
  );
}

The component is controlled. It owns selection, pointer gestures, live connector previews, and the WYSIWYG label input. Your application owns source state, persistence, undo, permissions, and collaboration.

In a Next.js App Router project, render it from a Client Component as shown above. Vite and other client-rendered React applications use the same component without the 'use client' directive. You do not need dynamic(..., { ssr: false }).

Complete React API

interface InteractiveEditDetail {
  reason: 'label' | 'position';
  item: SceneItem | null;
}

interface InteractiveSchematexDiagramProps {
  value: string;
  onChange: (value: string, detail: InteractiveEditDetail) => void;
  type?: DiagramType;
  theme?: string;
  fontFamily?: string;
  padding?: number;
  readOnly?: boolean;
  debounceMs?: number;
  className?: string;
  canvasClassName?: string;
  style?: React.CSSProperties;
  viewport?: boolean | ViewportOptions;
  onViewportChange?: (state: ViewportState) => void;
  viewportRef?: React.Ref<ViewportController>;
  ariaLabel?: string;
  labelEditorClassName?: string;
  labelEditorStyle?: React.CSSProperties;
  selectedKey?: string | null;
  onSelect?: (item: SceneItem | null) => void;
  onPreviewChange?: (value: string | null, detail: InteractiveEditDetail) => void;
  onRender?: (result: SchematexRenderResult) => void;
  onError?: (error: Error) => void;
}
PropDefaultContract
valuerequiredControlled Schematex DSL. Update it from onChange; the component never owns the canonical document.
onChangerequiredCalled synchronously after a label commit or completed position gesture. The second argument identifies the reason and selected semantic item.
typeauto-detectOptional canonical diagram type. Pass it when the surrounding UI has already chosen a type.
themerenderer defaultTheme name forwarded to renderResult().
fontFamilyrenderer defaultFont family forwarded to the diagram renderer.
paddingengine defaultOuter SVG padding in pixels.
readOnlyfalseKeeps the same controlled render surface but disables scene metadata, selection, label editing, and dragging.
debounceMs0Delays expensive rendering when value changes from an external source editor. Canvas commits still use revision guards.
classNamenoneClass on the outer focusable editor region.
canvasClassNamenoneClass on the inner canvas host <div> that directly contains the generated <svg>.
stylenoneInline style on the outer editor region.
viewportfalseOpts into host-layer pan, zoom, pinch, and fit. Pass true for safe defaults or an options object to override them.
onViewportChangeno-opCalled after a gesture or controller command changes { scale, x, y }. Viewport state is presentation-only and never enters the DSL.
viewportRefnoneRef to the imperative ViewportController used for application-owned zoom buttons, fit, reset, and programmatic pan.
ariaLabelEditable Schematex diagramAccessible name for the editor region. Use a document-specific label.
labelEditorClassNamesx-label-editorClass on the temporary WYSIWYG <input> rendered into document.body.
labelEditorStylenoneInline overrides merged after the measured WYSIWYG input styles.
selectedKeyuncontrolledOptional controlled SceneItem.key. Use it to mirror a source-editor cursor or an external inspector onto the canvas selection.
onSelectno-opCalled with the selected SceneItem, or null when selection clears.
onPreviewChangeno-opEmits a revision-guarded transient source document while a position gesture is active. Render it in a source editor, but persist only onChange; null discards a cancelled preview.
onRenderno-opCalled with every new SchematexRenderResult, including invalid preview results.
onErrorno-opCalled when parsing, layout, or rendering fails; the diagnostic fallback SVG remains visible.

SceneItem keys are stable edit targets for the current source revision. Do not persist a raw source range or assume that an item index remains stable after structural DSL edits.

Sizing and styling

canvasClassName names the SVG host, not the SVG itself. Target its direct SVG child when you want a responsive canvas:

<InteractiveSchematexDiagram
  value={dsl}
  onChange={setDsl}
  className="diagramEditor"
  canvasClassName="diagramCanvas"
/>
.diagramEditor {
  min-height: 28rem;
  border: 1px solid #e8eef6;
  overflow: auto;
}

.diagramCanvas {
  min-height: inherit;
  display: grid;
  place-items: center;
}

.diagramCanvas > svg {
  display: block;
  max-width: 100%;
  max-height: 100%;
}

The WYSIWYG input is portalled into document.body so it stays aligned when the canvas scrolls or its container clips overflow. Press Enter or blur to commit; press Escape to cancel.

Viewport

The viewport is opt-in so inline diagrams, SEO thumbnails, and existing card layouts do not change behavior after upgrading. Schematex provides the coordinate-safe transform and gestures; your application owns the controls:

import { useRef, useState } from 'react';
import { InteractiveSchematexDiagram } from 'schematex/react';
import type { ViewportController } from 'schematex/interactive';

const viewportRef = useRef<ViewportController>(null);
const [zoom, setZoom] = useState(100);

<InteractiveSchematexDiagram
  value={dsl}
  onChange={setDsl}
  viewport={{ wheelRequiresModifier: false }}
  viewportRef={viewportRef}
  onViewportChange={({ scale }) => setZoom(Math.round(scale * 100))}
/>;

<button onClick={() => viewportRef.current?.zoomOut()}>โˆ’</button>
<span>{zoom}%</span>
<button onClick={() => viewportRef.current?.zoomIn()}>+</button>
<button onClick={() => viewportRef.current?.fit()}>Fit</button>

wheelRequiresModifier defaults to true: Ctrl/Command + wheel zooms while a normal wheel keeps scrolling the page. macOS trackpad pinch already reports a Ctrl-modified wheel event. Full-screen canvases can set it to false. Drag unclaimed canvas space to pan, drag an editable node to move the node, and use two touch pointers to pinch. A second touch cancels an in-progress node drag before pinch begins, so a partial setPosition is never committed.

The controller exposes zoomIn, zoomOut, zoomTo, panBy, fit, reset, getState, and setState. Scale is clamped to 0.1โ€“10 by default. Initial fit is contain; ordinary DSL edits preserve the current viewport, while a diagram-type change fits the replacement diagram.

The transform belongs on the host <div>, never an SVG <g> or a rewritten viewBox. Schematex editing converts pointer coordinates with svg.getScreenCTM(), which includes ancestor CSS transforms but not an inner SVG group transform. Keeping the viewport outside the SVG is what makes a node dragged 80 screen pixels at 2ร— zoom move 40 diagram units.

Persistence, undo, and autosave

Treat each onChange value as the next complete document. A minimal undo and autosave integration looks like this:

const [dsl, setDsl] = useState(initialDsl);
const history = useRef<string[]>([initialDsl]);

function commit(nextDsl: string) {
  history.current.push(nextDsl);
  setDsl(nextDsl);
  queueAutosave(nextDsl);
}

<InteractiveSchematexDiagram value={dsl} onChange={commit} />;

For collaboration, broadcast the returned DSL or translate its change into your existing document/CRDT layer. Apply permissions with readOnly; the component does not implement authentication or server-side authorization.

Error and preview contract

The editor uses renderResult() rather than throwing into the React tree. Invalid DSL therefore produces a visible diagnostic SVG and an onError callback. Use onRender when the application also needs ok, status, diagnostics, or the detected diagram type:

<InteractiveSchematexDiagram
  value={dsl}
  onChange={setDsl}
  onRender={(result) => setIsValid(result.ok)}
  onError={(error) => reportEditorError(error)}
/>

Do not treat โ€œan SVG string existsโ€ as proof that the DSL is valid. Save, export, telemetry, and billing decisions should inspect the result status.

Vanilla DOM

import { renderResult } from 'schematex';
import {
  attachInteraction,
  attachViewport,
  sourceRevision,
} from 'schematex/interactive';

const frame = document.querySelector<HTMLElement>('#diagram-frame')!;
const host = frame.querySelector<HTMLElement>('#diagram-host')!;
let source = initialDsl;
let result = renderResult(source, { scene: true });
host.innerHTML = result.svg;
const viewport = attachViewport(frame, host, {
  wheelRequiresModifier: true,
});

if (result.ok) {
  attachInteraction(host.querySelector('svg')!, {
    getSource: () => source,
    getScene: () => ({
      rev: sourceRevision(source),
      items: result.scene ?? [],
    }),
    onSourceChange: (nextSource) => {
      source = nextSource;
      rerender();
    },
  });
}

Use the React component unless you need to integrate gestures into an existing non-React canvas. attachInteraction() is deliberately low level; its caller must rerender after a committed edit and detach listeners before replacing the SVG. Call viewport.detach() when the frame is removed.

Capability discovery

import {
  getInteractiveCapabilities,
  INTERACTIVE_DIAGRAM_COUNT,
  POSITION_EDITABLE_DIAGRAM_COUNT,
} from 'schematex';

getInteractiveCapabilities('sequence');
// {
//   type: 'sequence',
//   text: ['title', 'labels'],
//   position: 'move-x',
//   reason: 'Horizontal lifeline order is editable, while the vertical axis โ€ฆ'
// }

console.log(INTERACTIVE_DIAGRAM_COUNT);         // 21
console.log(POSITION_EDITABLE_DIAGRAM_COUNT);   // 18

INTERACTIVE_DIAGRAM_COUNT is 21: only diagram types with parser-native source ranges are advertised as canvas-editable. Eighteen also expose a position model. The other 31 diagram types remain fully renderable and editable through the DSL source, but return empty text plus position: "none" and emit no guessed canvas handles. A value such as free, move-x, cross-axis, or native-xy is a semantic constraint, not a UI hint. reason explains the standard or layout rule behind that constraint and is the canonical copy for product UI, documentation, and AI integrations.

For web agents and non-JavaScript tooling, the same registry is available as machine-readable JSON. The complete human capability matrix documents each diagram's axes, editable fields, routing behavior, and limits.

How edits persist

  • Stable semantic objects persist presentation-only positions under @overrides, for example pin R1 153.1,73.1.
  • Domain geometryโ€”dates, room sizes, floorplan furniture, site coordinates, breadboard holes, waveform boundariesโ€”rewrites its native DSL token.
  • Authored labels replace only their exact source range. Computed labels and identifiers without a safe rename contract remain read-only.
  • Core records the source revision and exact expected text on every editable scene item. setLabel fails closed if either changed; callers must rerender instead of retrying an old target.
  • Connected lines preview during drag and are recomputed from semantic endpoints after drop. Engines that promise orthogonal routing remain horizontal/vertical after the rerender.

The interactive workspace includes published examples for all 52 diagram families. Its capability bar is generated from the same registry, so parser-native canvas editing and source-only behavior cannot drift apart.

AI-safe edits

AI agents should never invent source offsets. The schematex/ai package and both MCP transports expose this revision-guarded flow:

import { inspectDiagram, applyDiagramEdits } from 'schematex/ai';

const inspected = inspectDiagram('flowchart', dsl);
if (inspected.ok) {
  const result = applyDiagramEdits('flowchart', dsl, inspected.revision, [
    { target: inspected.items[0].key, op: 'setLabel', value: 'Approved' },
  ]);
}

The batch is atomic: a stale revision, unknown target, unsafe operation, or invalid final render returns the original DSL instead of a partial mutation.

Read-only React renderer

When the canvas must never modify DSL, use the smaller read-only component:

import { SchematexDiagram } from 'schematex/react';

<SchematexDiagram
  dsl={dsl}
  type="circuit"
  viewport
  onError={(error) => console.error(error)}
/>;

SchematexDiagram accepts the same viewport, onViewportChange, and viewportRef props. When viewport is omitted it keeps its original single-wrapper DOM structure.

Schematex is AGPL-3.0. Closed-source commercial products should review the licensing notes before distribution.

Found this useful?

Schematex is free, fully open source, and zero-dependency. A star helps other developers discover it.