npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-svg-canvas

v0.1.4

Published

React library for building interactive SVG canvas applications with pan, zoom, selection, drag-and-drop, resize, and Figma-style snapping

Readme

react-svg-canvas

A React library for building interactive SVG canvas applications with pan, zoom, selection, drag-and-drop, resize, and Figma-style snapping.

Features

  • Pan & Zoom - Middle mouse/touch panning, mouse wheel/pinch-to-zoom
  • Touch Support - Full touch event handling for mobile devices
  • Selection System - Multi-select, rectangle selection, selection bounds
  • Drag & Drop - Smooth dragging with window-level event handling
  • Resize Handles - 8-point resize with min/max constraints
  • Rotation - Object and group rotation with snap angles and pivot point manipulation
  • Snapping - Figma-style snapping to edges, centers, grid, and matching sizes
  • Geometry Utilities - Bounds operations, transforms, coordinate conversion
  • Spatial Queries - Hit testing, rectangle selection, culling
  • TypeScript - Full type definitions included

Installation

npm install react-svg-canvas
# or
pnpm add react-svg-canvas
# or
yarn add react-svg-canvas

Peer Dependencies: React 18+

Quick Start

import { SvgCanvas, useSvgCanvas } from 'react-svg-canvas'

function MyCanvas() {
  return (
    <SvgCanvas
      className="my-canvas"
      style={{ width: '100%', height: '100%' }}
    >
      <rect x={100} y={100} width={200} height={150} fill="#3b82f6" />
      <circle cx={400} cy={200} r={50} fill="#ef4444" />
    </SvgCanvas>
  )
}

API Reference

SvgCanvas

The main canvas component that provides pan, zoom, and coordinate transformation.

import { SvgCanvas, SvgCanvasHandle } from 'react-svg-canvas'

function App() {
  const canvasRef = useRef<SvgCanvasHandle>(null)

  return (
    <SvgCanvas
      ref={canvasRef}
      className="canvas"
      style={{ width: '100vw', height: '100vh' }}
      fixed={<MyToolbar />}  // Renders in screen space (not transformed)
      onToolStart={(e) => console.log('Tool start:', e.x, e.y)}
      onToolMove={(e) => console.log('Tool move:', e.x, e.y)}
      onToolEnd={() => console.log('Tool end')}
      onContextReady={(ctx) => console.log('Scale:', ctx.scale)}
    >
      {/* Children render in canvas space (transformed) */}
      <MyShapes />
    </SvgCanvas>
  )
}

Props

| Prop | Type | Description | |------|------|-------------| | className | string | CSS class for the SVG element | | style | CSSProperties | Inline styles for the SVG element | | children | ReactNode | Content rendered in canvas space (pan/zoom applied) | | fixed | ReactNode | Content rendered in screen space (UI overlays) | | onToolStart | (e: ToolEvent) => void | Called on left mouse/touch start | | onToolMove | (e: ToolEvent) => void | Called during drag | | onToolEnd | () => void | Called on mouse/touch end | | onContextReady | (ctx: SvgCanvasContext) => void | Called when context changes (zoom, pan) |

Imperative Handle

const canvasRef = useRef<SvgCanvasHandle>(null)

// Center viewport on a point
canvasRef.current?.centerOn(100, 100, 1.5)  // x, y, optional zoom

// Fit a rectangle in view
canvasRef.current?.centerOnRect(0, 0, 500, 400, 50)  // x, y, w, h, padding

// Get/set transform matrix
const matrix = canvasRef.current?.getMatrix()
canvasRef.current?.setMatrix([1, 0, 0, 1, 0, 0])

Interaction Controls

| Input | Action | |-------|--------| | Left mouse | Tool events (onToolStart/Move/End) | | Middle mouse | Pan canvas | | Mouse wheel | Zoom in/out | | Single touch | Tool events or pan | | Two-finger pinch | Zoom |


useSvgCanvas

Hook to access canvas context from child components.

function MyShape() {
  const { svg, matrix, scale, translateTo, translateFrom } = useSvgCanvas()

  // Convert screen coords to canvas coords
  const [canvasX, canvasY] = translateTo(screenX, screenY)

  // Convert canvas coords to screen coords
  const [screenX, screenY] = translateFrom(canvasX, canvasY)

  return <rect x={100} y={100} width={100 / scale} height={100 / scale} />
}

Selection System

useSelection

Manages selection state for canvas objects.

import { useSelection, SpatialObject } from 'react-svg-canvas'

interface MyObject extends SpatialObject {
  id: string
  bounds: Bounds
  color: string
}

function Canvas({ objects }: { objects: MyObject[] }) {
  const {
    selectedIds,
    selectedObjects,
    selectionCount,
    selectionBounds,
    hasSelection,
    select,
    selectMultiple,
    deselect,
    toggle,
    clear,
    selectAll,
    selectInRect,
    setSelection,
    isSelected
  } = useSelection({ objects, onChange: (ids) => console.log('Selection:', ids) })

  return (
    <SvgCanvas>
      {objects.map(obj => (
        <rect
          key={obj.id}
          {...obj.bounds}
          fill={isSelected(obj.id) ? 'blue' : obj.color}
          onClick={(e) => select(obj.id, e.shiftKey)}
        />
      ))}
      {selectionBounds && (
        <SelectionBox bounds={selectionBounds} onResizeStart={handleResize} />
      )}
    </SvgCanvas>
  )
}

SelectionBox

Renders a selection rectangle with resize handles.

import { SelectionBox } from 'react-svg-canvas'

<SelectionBox
  bounds={{ x: 100, y: 100, width: 200, height: 150 }}
  rotation={45}
  stroke="#0066ff"
  strokeDasharray="4,4"
  showHandles={true}
  handleSize={8}
  onResizeStart={(handle, e) => console.log('Resize:', handle)}
/>

Interaction Hooks

useDraggable

Provides smooth drag interaction with window-level events.

import { useDraggable, svgTransformCoordinates } from 'react-svg-canvas'

function DraggableRect({ x, y, onMove }) {
  const { isDragging, dragProps } = useDraggable({
    onDragStart: (e) => console.log('Start:', e.x, e.y),
    onDragMove: (e) => onMove(e.deltaX, e.deltaY),
    onDragEnd: (e) => console.log('End'),
    transformCoordinates: svgTransformCoordinates  // For SVG coordinate space
  })

  return (
    <rect
      x={x} y={y}
      width={100} height={80}
      fill={isDragging ? 'orange' : 'blue'}
      style={{ cursor: 'move' }}
      {...dragProps}
    />
  )
}

useResizable

Provides resize interaction for selected objects.

import { useResizable } from 'react-svg-canvas'

function ResizableRect({ bounds, onResize }) {
  const { isResizing, activeHandle, handleResizeStart } = useResizable({
    bounds,
    minWidth: 50,
    minHeight: 50,
    onResize: (e) => onResize(e.bounds),
    onResizeEnd: (e) => console.log('Final bounds:', e.bounds)
  })

  return (
    <SelectionBox
      bounds={bounds}
      onResizeStart={handleResizeStart}
    />
  )
}

Snapping System

Figma-style snapping with visual guide lines.

useSnapping

import { useSnapping, SnapGuides, DEFAULT_SNAP_CONFIG } from 'react-svg-canvas'

function Canvas({ objects }) {
  const { svg, translateFrom } = useSvgCanvas()
  const viewBounds = { x: 0, y: 0, width: 1000, height: 800 }

  const { snapDrag, snapResize, activeSnaps, allCandidates, clearSnaps } = useSnapping({
    objects,
    config: DEFAULT_SNAP_CONFIG,
    viewBounds
  })

  function handleDrag(objectId, bounds, delta, grabPoint) {
    const result = snapDrag({
      bounds: { ...bounds, rotation: 0 },
      objectId,
      delta,
      grabPoint
    })
    // result.position contains snapped coordinates
    // result.activeSnaps contains active snap info
  }

  return (
    <SvgCanvas
      fixed={
        <SnapGuides
          activeSnaps={activeSnaps}
          config={DEFAULT_SNAP_CONFIG.guides}
          viewBounds={viewBounds}
          transformPoint={translateFrom}
        />
      }
    >
      {/* Your objects */}
    </SvgCanvas>
  )
}

useGrabPoint

Helper hook for calculating the normalized grab point when dragging objects.

import { useGrabPoint } from 'react-svg-canvas'

function MyDraggable({ bounds }) {
  const { setGrabPoint, getGrabPoint } = useGrabPoint()

  function handleDragStart(mousePos) {
    setGrabPoint(mousePos, bounds)
  }

  function handleDrag(delta) {
    const grabPoint = getGrabPoint()  // Returns { x: 0-1, y: 0-1 }
    // Use with snapDrag...
  }
}

Snap Configuration

const config: SnapConfiguration = {
  enabled: true,
  snapToGrid: true,
  snapToObjects: true,
  snapToSizes: true,         // Snap to matching widths/heights
  gridSize: 10,
  snapThreshold: 8,          // Pixels within which snapping activates
  weights: {
    distance: 10,            // How much distance affects snap priority
    direction: 3,            // Movement direction influence
    velocity: 2,             // Faster movement = less sticky
    grabProximity: 5,        // Snaps near grab point prioritized
    hierarchy: 4,            // Parent/sibling preference
    edgePriority: 1.2,
    centerPriority: 1.0,
    gridPriority: 0.8,
    sizePriority: 0.9
  },
  guides: {
    color: '#ff3366',
    strokeWidth: 1,
    showDistanceIndicators: true
  },
  debug: {
    enabled: false,
    showTopN: 5,
    showScores: true,
    showScoreBreakdown: false
  }
}

Rotation System

Object and group rotation with visual snap zones and pivot point manipulation.

useRotatable

Provides rotation interaction with visual snap zones. When the pointer is within the inner portion of the rotation arc (default 75%), angles snap to predefined values.

import { useRotatable, DEFAULT_SNAP_ANGLES } from 'react-svg-canvas'

function RotatableObject({ bounds, rotation, onRotate }) {
  const { translateTo, translateFrom } = useSvgCanvas()

  const {
    rotationState,
    handleRotateStart,
    rotateProps,
    checkSnapZone,
    arcRadius,
    pivotPosition
  } = useRotatable({
    bounds,
    rotation,
    pivotX: 0.5,
    pivotY: 0.5,
    snapAngles: DEFAULT_SNAP_ANGLES,  // 15° intervals: [0, 15, 30, ...]
    snapZoneRatio: 0.75,              // Inner 75% of arc triggers snapping
    translateTo,
    translateFrom,
    screenSpaceSnapZone: true,        // Consistent UX at all zoom levels
    onRotate: (angle, isSnapped) => onRotate(angle),
    onRotateEnd: (angle) => console.log('Final:', angle)
  })

  return (
    <g>
      <rect {...bounds} />
      <RotationHandle
        position={pivotPosition}
        arcRadius={arcRadius}
        isInSnapZone={rotationState.isInSnapZone}
        onPointerDown={handleRotateStart}
        {...rotateProps}
      />
    </g>
  )
}
CRDT-Friendly State

For collaborative editing with external state (e.g., Yjs), use getter functions to avoid stale closures:

const { handleRotateStart } = useRotatable({
  bounds,
  rotation,
  // Getters are called at drag start for fresh values
  getBounds: () => yObject.get('bounds'),
  getRotation: () => yObject.get('rotation'),
  getPivot: () => ({ x: yObject.get('pivotX'), y: yObject.get('pivotY') }),
  onRotate: (angle) => yObject.set('rotation', angle)
})

usePivotDrag

Drag interaction for manipulating an object's rotation pivot point.

import { usePivotDrag, DEFAULT_PIVOT_SNAP_POINTS } from 'react-svg-canvas'

function PivotHandle({ bounds, rotation, pivotX, pivotY, onPivotChange }) {
  const { translateTo } = useSvgCanvas()

  const {
    pivotState,
    handlePivotDragStart,
    pivotDragProps,
    getPositionCompensation
  } = usePivotDrag({
    bounds,
    rotation,
    pivotX,
    pivotY,
    snapPoints: DEFAULT_PIVOT_SNAP_POINTS,  // 9 points: corners, edges, center
    snapThreshold: 0.08,
    translateTo,
    onDrag: (pivot, snappedPoint, positionCompensation) => {
      // positionCompensation adjusts object position to keep it visually in place
      onPivotChange(pivot, positionCompensation)
    },
    onDragEnd: (pivot, positionCompensation) => {
      console.log('Final pivot:', pivot)
    }
  })

  return (
    <circle
      cx={bounds.x + bounds.width * pivotX}
      cy={bounds.y + bounds.height * pivotY}
      r={6}
      fill={pivotState.snappedPoint ? 'blue' : 'gray'}
      onPointerDown={handlePivotDragStart}
      {...pivotDragProps}
    />
  )
}

useGroupPivot

Manages a shared pivot point for rotating multiple selected objects together.

import { useGroupPivot } from 'react-svg-canvas'

function GroupRotationUI({ selectedObjects, selectionBounds }) {
  const {
    groupPivotState,
    groupPivot,
    handleGroupPivotDragStart,
    groupPivotDragProps,
    rotateObjectsAroundPivot,
    resetPivotToCenter,
    setGroupPivot
  } = useGroupPivot({
    objects: selectedObjects,  // Array of { id, bounds, rotation, pivotX?, pivotY? }
    selectionBounds,
    onRotate: (angle, transformedObjects) => {
      // transformedObjects: { id, x, y, rotation }[]
      updateObjects(transformedObjects)
    }
  })

  return (
    <>
      {/* Pivot handle */}
      <circle
        cx={groupPivot.x}
        cy={groupPivot.y}
        r={8}
        fill={groupPivotState.isPivotCustom ? 'orange' : 'white'}
        onPointerDown={handleGroupPivotDragStart}
        {...groupPivotDragProps}
      />

      {/* Reset button */}
      <button onClick={resetPivotToCenter}>Reset Pivot</button>
    </>
  )
}

Rotation Utilities

import {
  // Constants
  DEFAULT_SNAP_ANGLES,           // [0, 15, 30, 45, ..., 345]
  DEFAULT_SNAP_ZONE_RATIO,       // 0.75
  DEFAULT_PIVOT_SNAP_THRESHOLD,  // 0.08

  // Angle utilities
  getAngleFromCenter,            // (center, point) => degrees
  snapAngle,                     // (angle, snapAngles, isInSnapZone) => angle
  findClosestSnapAngle,          // (angle, snapAngles) => snapAngle

  // Pivot utilities
  getPivotPosition,              // (bounds, pivotX, pivotY) => Point
  calculatePivotCompensation,    // Position adjustment when pivot moves
  canvasToPivot,                 // Convert canvas coords to normalized pivot
  snapPivot,                     // Snap to nearest snap point

  // Rotation transforms
  rotatePointAroundCenter,       // (point, center, angleDeg) => Point
  rotateObjectAroundPivot        // Transform object position during rotation
} from 'react-svg-canvas'

Geometry Utilities

Bounds Operations

import {
  getBoundsCenter,
  expandBounds,
  unionBounds,
  unionAllBounds,
  boundsIntersect,
  boundsContains,
  pointInBounds,
  boundsFromPoints,
  getHandlePositions,
  resizeBounds
} from 'react-svg-canvas'

// Get center point
const center = getBoundsCenter({ x: 0, y: 0, width: 100, height: 100 })
// { x: 50, y: 50 }

// Expand bounds by margin
const expanded = expandBounds(bounds, 10)

// Union of two bounds
const combined = unionBounds(boundsA, boundsB)

// Check intersection
if (boundsIntersect(selection, object.bounds)) {
  // Object is selected
}

// Create bounds from drag rectangle
const selectionRect = boundsFromPoints(startPoint, endPoint)

// Resize bounds from handle drag
const newBounds = resizeBounds(originalBounds, 'se', deltaX, deltaY, minW, minH)

Transforms

import {
  transformPoint,
  invertTransform,
  composeTransforms,
  matrixToTransform,
  transformToMatrix,
  getAbsolutePosition
} from 'react-svg-canvas'

// Apply transform to point
const worldPoint = transformPoint({ x: 10, y: 10 }, transform)

// Convert SVG matrix to transform object
const transform = matrixToTransform([1, 0, 0, 1, 100, 50])

// Get absolute position walking up hierarchy
const absPos = getAbsolutePosition(item, (item) => itemsById[item.parentId])

Math Utilities

import {
  rotatePoint,
  scalePoint,
  distance,
  snapToGrid,
  snapPointToGrid,
  lerp,
  clamp,
  normalizeAngle,
  degToRad,
  radToDeg
} from 'react-svg-canvas'

// Rotate point around center
const rotated = rotatePoint({ x: 100, y: 0 }, { x: 0, y: 0 }, 90)

// Snap to grid
const snapped = snapPointToGrid({ x: 123, y: 456 }, 10)
// { x: 120, y: 460 }

Spatial Queries

import {
  getObjectsAtPoint,
  getTopmostAtPoint,
  getObjectsIntersectingRect,
  getObjectsContainedInRect,
  getSelectionBounds,
  getObjectsInView,
  findNearestObject,
  getObjectsInRadius
} from 'react-svg-canvas'

// Hit testing
const clicked = getTopmostAtPoint(objects, { x: mouseX, y: mouseY })

// Rectangle selection
const selected = getObjectsIntersectingRect(objects, selectionRect)

// Viewport culling (render only visible objects)
const visible = getObjectsInView(objects, viewBounds)

// Find nearest object
const nearest = findNearestObject(objects, cursorPos, maxDistance)

Types

interface Point {
  x: number
  y: number
}

interface Bounds {
  x: number
  y: number
  width: number
  height: number
}

interface Transform {
  x: number
  y: number
  rotation: number
  scaleX: number
  scaleY: number
}

interface SpatialObject {
  id: string
  bounds: Bounds
}

interface ToolEvent {
  startX: number
  startY: number
  x: number
  y: number
}

type ResizeHandle = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'

// Rotation types
interface RotationState {
  isRotating: boolean
  startAngle: number
  currentAngle: number
  centerX: number
  centerY: number
  isInSnapZone: boolean
}

interface PivotState {
  isDragging: boolean
  pivotX: number           // 0-1 normalized
  pivotY: number           // 0-1 normalized
  snappedPoint: Point | null
}

interface GroupPivotState {
  isDragging: boolean
  pivotX: number           // Canvas coordinates
  pivotY: number           // Canvas coordinates
  isPivotCustom: boolean   // User moved pivot from default center
}

Example: Complete Editor

import {
  SvgCanvas,
  SvgCanvasHandle,
  useSelection,
  useDraggable,
  useSnapping,
  SelectionBox,
  SnapGuides,
  DEFAULT_SNAP_CONFIG
} from 'react-svg-canvas'

function Editor() {
  const canvasRef = useRef<SvgCanvasHandle>(null)
  const [objects, setObjects] = useState<MyObject[]>(initialObjects)

  const selection = useSelection({
    objects,
    onChange: (ids) => console.log('Selected:', ids)
  })

  const snapping = useSnapping({
    objects,
    config: DEFAULT_SNAP_CONFIG,
    viewBounds: { x: 0, y: 0, width: 1920, height: 1080 }
  })

  return (
    <SvgCanvas
      ref={canvasRef}
      style={{ width: '100%', height: '100vh' }}
      onToolStart={(e) => {
        const hit = getTopmostAtPoint(objects, e)
        if (hit) selection.select(hit.id, false)
        else selection.clear()
      }}
      fixed={
        <SnapGuides
          activeSnaps={snapping.activeSnaps}
          config={DEFAULT_SNAP_CONFIG.guides}
          viewBounds={viewBounds}
        />
      }
    >
      {objects.map(obj => (
        <DraggableShape
          key={obj.id}
          object={obj}
          isSelected={selection.isSelected(obj.id)}
          onMove={(delta) => {
            const result = snapping.snapDrag({
              bounds: obj.bounds,
              objectId: obj.id,
              delta,
              grabPoint: { x: 0.5, y: 0.5 }
            })
            updateObject(obj.id, result.position)
          }}
        />
      ))}

      {selection.selectionBounds && (
        <SelectionBox
          bounds={selection.selectionBounds}
          onResizeStart={handleResize}
        />
      )}
    </SvgCanvas>
  )
}

Browser Support

  • Modern browsers with ES2021 support
  • Touch devices (iOS Safari, Android Chrome)

License

MIT License - see LICENSE for details.

Author

Szilard Hajba [email protected]