Resizable Node Views
A small, framework-agnostic NodeView that wraps any HTMLElement (image, iframe, video…) and adds configurable resize handles. It manages user interaction, applies min/max constraints, optionally preserves aspect ratio, and exposes callbacks for live updates and commits.
What is a Resizable Node View?
ResizableNodeView is a ProseMirror/Tiptap-compatible NodeView that:
- Wraps your element in a container + wrapper
- Adds configurable resize handles (corners and edges)
- Emits
onResizecontinuously while dragging - Emits
onCommitonce when the user finishes resizing (use this to persist new attributes) - Supports min/max constraints, aspect-ratio locking (config / Shift key), and class customization
- Adds a
data-resize-stateattribute and optionalresizingcss class while active
Options
element: HTMLElement— the element to make resizable (required)contentElement?: HTMLElement— optional HTML node to use as contentElement (for contenteditable nodes)node: Node— the ProseMirror node (required)getPos: () => number | undefined— function that returns the node position (required to persist)onResize?: (width: number, height: number) => void— optional, called continuously while draggingonCommit: (width: number, height: number) => void— called once when the drag finishesonUpdate?: NodeView['update']— optional node update handleroptions?: object— optional configuration (see below)
options properties:
-
directions?: ResizableNodeViewDirection[]Default:['bottom-left', 'bottom-right', 'top-left', 'top-right']Allowed:'top' | 'right' | 'bottom' | 'left' | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' -
min?: Partial<{ width: number; height: number; }>Default:{ width: 8, height: 8 }(pixels) -
max?: Partial<{ width: number; height: number; }>Default:undefined(no max) -
preserveAspectRatio?: booleanDefault:falseWhentruealways preserves aspect ratio. Whenfalse, pressingShiftwhile dragging temporarily preserves aspect ratio. -
className?: { container?: string; wrapper?: string; handle?: string; resizing?: string }Optional class names applied to container, wrapper, each handle, and a class added while actively resizing.
Callbacks
onResize(width, height): update the element visually (style.width/height) while dragging.onCommit(width, height): persist final dimensions (e.g.,editor.commands.updateAttributes(...)).onUpdate(node, decorations, innerDecorations): returntrueto accept updates orfalseto re-create the node view.
Example pattern to persist sizes inside onCommit:
const pos = getPos()
if (pos !== undefined) {
editor.commands.updateAttributes('image', { width, height })
}Usage example
Minimal image extension node view:
// inside addNodeView()
return ({ node, getPos, HTMLAttributes }) => {
const img = document.createElement('img')
img.src = HTMLAttributes.src
// copy non-size attributes to element
Object.entries(HTMLAttributes).forEach(([key, value]) => {
if (value == null) return
if (key === 'width' || key === 'height') return
img.setAttribute(key, String(value))
})
// instantiate ResizableNodeView
return new ResizableNodeView({
element: img,
node,
getPos,
onResize: (w, h) => {
img.style.width = `${w}px`
img.style.height = `${h}px`
},
onCommit: (w, h) => {
const pos = getPos()
if (pos === undefined) return
// persist new size to the node
editor.commands.updateAttributes('image', { width: w, height: h })
},
onUpdate: (updatedNode) => {
if (updatedNode.type !== node.type) return false
return true
},
options: {
directions: ['bottom-right', 'bottom-left', 'top-right', 'top-left'],
min: { width: 50, height: 50 },
preserveAspectRatio: false, // hold Shift to lock aspect ratio
className: {
container: 'my-resize-container',
wrapper: 'my-resize-wrapper',
handle: 'my-resize-handle',
resizing: 'is-resizing',
},
},
})
}Notes:
- The class does not inject visual styles; supply CSS for
[data-resize-handle],.is-resizing, etc. (demos provide minimal styles). - Currently contentEditable nodes are not fully supported and won't resize their content.
Behavior details & edge cases
- Aspect-ratio + constraints: when aspect ratio is preserved, constraints snap the dimension that hits a min/max first and compute the other dimension proportionally — the ratio is not broken.
- Shift key: while resizing, pressing Shift toggles a temporary aspect-ratio lock (when
preserveAspectRatioisfalse).