dragstack
v0.0.10
Published
A lightweight, performant React library for native drag, drop, and resize functionality
Maintainers
Readme
🎨 Dragstack - Lightweight Draggable Library
⚠️ Important: This library requires React 18.x and Tailwind CSS. React 19 is not yet supported.
Create beautiful, interactive canvas experiences with drag-and-drop simplicity! A lightweight, performant React library that makes building canvas editors as easy as drawing on paper.
🎮 Live Demo
Experience Dragstack in action! Add shapes, drag them around, resize, and see how smooth and intuitive the library is.
✨ Why Dragstack?
🚀 Lightning Fast - Built with native browser APIs for buttery-smooth 60fps interactions
🎯 All-in-One Solution - Canvas, grid, drag, drop, and resize in one powerful package
🎨 Beautiful by Default - Smooth shadows, animations, and visual feedback out of the box
🔧 Developer Friendly - Simple API with TypeScript support and comprehensive docs
♿ Accessible - Full ARIA support and keyboard navigation
📱 Responsive - Works perfectly on desktop, tablet, and mobile devices
🚀 Quick Start
Prerequisites
⚠️ Important Requirements:
- React 18.x - This library is built for React 18. React 19 is not yet supported due to breaking changes in hooks API
- Tailwind CSS - Required for styling components. Make sure Tailwind is configured in your project.
Installation
# Install React 18 (if not already installed)
npm install react@^18.0.0 react-dom@^18.0.0
# Install Tailwind CSS (if not already installed)
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Install Dragstack
npm install dragstack
# or
yarn add dragstack
# or
pnpm add dragstackYour First Canvas in 30 Seconds 🎯
import React from 'react';
import { Canvas, useDraggableState } from 'dragstack';
const MyFirstCanvas = () => {
const {
elements,
selectedElement,
addElement,
removeElement,
updateElementPosition,
updateElementSize,
selectElement
} = useDraggableState({
initialElements: [
{
id: 'my-first-shape',
type: 'rectangle',
position: { x: 100, y: 100 },
size: { width: 200, height: 150 },
zIndex: 1,
properties: {
fill: '#3b82f6',
opacity: 1
}
}
]
});
return (
<Canvas
elements={elements}
selectedElement={selectedElement}
onUpdateElementPosition={updateElementPosition}
onUpdateElementSize={updateElementSize}
onSelectElement={selectElement}
width="100%"
height={600}
enableGrid={true}
showGrid={true}
gridSize={20}
constrainToCanvas={true}
draggableProps={{
enableDragShadow: true,
enableResizeShadow: true,
dragScale: 1.02,
resizeScale: 1.01
}}
>
{(element, isSelected) => (
<div
style={{
width: '100%',
height: '100%',
backgroundColor: element.properties?.fill,
border: isSelected ? '3px solid #1f2937' : '2px solid #e5e7eb',
borderRadius: '12px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '14px',
fontWeight: 'bold',
transition: 'all 0.2s ease',
cursor: 'move'
}}
>
Drag me! 🎯
</div>
)}
</Canvas>
);
};
export default MyFirstCanvas;That's it! You now have a fully functional draggable canvas with:
- ✅ Drag and drop functionality
- ✅ Resize handles on selection
- ✅ Grid snapping
- ✅ Visual feedback with shadows
- ✅ Boundary constraints
- ✅ Smooth animations
🎮 Interactive Playground
Want to see it in action? Here's a complete interactive example:
import React, { useState } from 'react';
import { Canvas, useDraggableState } from 'dragstack';
const InteractiveCanvas = () => {
const [shapeType, setShapeType] = useState('rectangle');
const [shapeColor, setShapeColor] = useState('#3b82f6');
const canvasState = useDraggableState({
initialElements: []
});
const addShape = () => {
const newShape = {
id: `${shapeType}-${Date.now()}`,
type: shapeType,
position: {
x: Math.random() * 400 + 50,
y: Math.random() * 300 + 50
},
size: {
width: shapeType === 'circle' ? 120 : 150,
height: shapeType === 'circle' ? 120 : 100
},
zIndex: canvasState.elements.length + 1,
properties: {
fill: shapeColor,
opacity: 0.9
}
};
canvasState.addElement(newShape);
};
const clearCanvas = () => {
canvasState.elements.forEach(element => {
canvasState.removeElement(element.id);
});
};
return (
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
{/* Control Panel */}
<div style={{
marginBottom: '20px',
padding: '15px',
backgroundColor: '#f8fafc',
borderRadius: '10px',
display: 'flex',
gap: '15px',
alignItems: 'center',
flexWrap: 'wrap'
}}>
<div>
<label style={{ marginRight: '10px', fontWeight: 'bold' }}>Shape:</label>
<select
value={shapeType}
onChange={(e) => setShapeType(e.target.value)}
style={{ padding: '8px', borderRadius: '5px', border: '1px solid #d1d5db' }}
>
<option value="rectangle">Rectangle</option>
<option value="circle">Circle</option>
<option value="square">Square</option>
</select>
</div>
<div>
<label style={{ marginRight: '10px', fontWeight: 'bold' }}>Color:</label>
<input
type="color"
value={shapeColor}
onChange={(e) => setShapeColor(e.target.value)}
style={{ width: '50px', height: '35px', borderRadius: '5px', border: 'none' }}
/>
</div>
<button
onClick={addShape}
style={{
padding: '10px 20px',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '8px',
fontWeight: 'bold',
cursor: 'pointer'
}}
>
➕ Add Shape
</button>
<button
onClick={clearCanvas}
style={{
padding: '10px 20px',
backgroundColor: '#ef4444',
color: 'white',
border: 'none',
borderRadius: '8px',
fontWeight: 'bold',
cursor: 'pointer'
}}
>
🗑️ Clear Canvas
</button>
<span style={{ fontSize: '14px', color: '#6b7280' }}>
Elements: {canvasState.elements.length} |
{canvasState.selectedElement && ` Selected: ${canvasState.selectedElement.type}`}
</span>
</div>
{/* Canvas */}
<Canvas
elements={canvasState.elements}
selectedElement={canvasState.selectedElement}
onUpdateElementPosition={canvasState.updateElementPosition}
onUpdateElementSize={canvasState.updateElementSize}
onSelectElement={(elementId) => {
canvasState.selectElement(elementId);
canvasState.bringToFront(elementId);
}}
width="100%"
height={600}
enableGrid={true}
showGrid={true}
gridSize={20}
constrainToCanvas={true}
draggableProps={{
enableDragShadow: true,
enableResizeShadow: true,
dragScale: 1.05,
resizeScale: 1.02
}}
>
{(element, isSelected) => {
const baseStyle = {
width: '100%',
height: '100%',
backgroundColor: element.properties?.fill,
border: isSelected ? '3px solid #1f2937' : '2px solid #e5e7eb',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '12px',
fontWeight: 'bold',
cursor: 'move',
userSelect: 'none',
transition: 'all 0.2s ease'
};
if (element.type === 'circle') {
return (
<div style={{ ...baseStyle, borderRadius: '50%' }}>
{isSelected ? '✨ Selected' : 'Circle'}
</div>
);
}
if (element.type === 'square') {
return (
<div style={{ ...baseStyle, borderRadius: '8px' }}>
{isSelected ? '✨ Selected' : 'Square'}
</div>
);
}
return (
<div style={{ ...baseStyle, borderRadius: '12px' }}>
{isSelected ? '✨ Selected' : 'Rectangle'}
</div>
);
}}
</Canvas>
{/* Instructions */}
<div style={{
marginTop: '20px',
padding: '15px',
backgroundColor: '#f0f9ff',
borderRadius: '10px',
border: '1px solid #bfdbfe'
}}>
<h3 style={{ margin: '0 0 10px 0', color: '#1e40af' }}>🎮 How to Use:</h3>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#1e40af' }}>
<li>Click "Add Shape" to create new elements</li>
<li>Drag elements to move them around</li>
<li>Select an element to see resize handles</li>
<li>Drag the handles to resize</li>
<li>Elements automatically snap to the grid</li>
<li>Elements stay within canvas boundaries</li>
</ul>
</div>
</div>
);
};
export default InteractiveCanvas;🎯 Key Features & Use Cases
🎨 Design Tools
Build tools like Figma, Canva, or Sketch with intuitive drag-and-drop interfaces.
// Perfect for building:
- ✨ Diagram editors
- 🎨 Design systems
- 📊 Chart builders
- 🗺️ Map editors
- 📝 Form builders📊 Dashboard Builders
Create interactive dashboards with draggable widgets and charts.
// Dashboard components:
- 📈 Charts & graphs
- 📋 Cards & widgets
- 🔧 Settings panels
- 📱 Layout editors🎮 Educational Tools
Build interactive learning environments and simulations.
// Educational applications:
- 🧩 Puzzle games
- 📐 Geometry tools
- 🎯 Quiz builders
- 🗺️ Interactive maps🔧 Component API
🎨 Canvas Component
The Canvas component is your all-in-one solution for creating beautiful drag-and-drop interfaces. Think of it as a digital workspace where users can intuitively move, resize, and organize elements.
<Canvas
// Canvas properties
width="100%"
height={600}
className="my-canvas"
disabled={false}
// Grid settings
enableGrid={true}
showGrid={true}
gridSize={20}
gridColor="#e5e7eb"
// Element management
elements={elements}
selectedElement={selectedElement}
onUpdateElementPosition={updatePosition}
onUpdateElementSize={updateSize}
onSelectElement={selectElement}
// Constraints
constrainToCanvas={true}
// Visual effects
draggableProps={{
enableDragShadow: true,
enableResizeShadow: true,
dragScale: 1.02,
resizeScale: 1.01
}}
>
{(element, isSelected) => (
// Your custom component here
<div>Your content</div>
)}
</Canvas>📋 Canvas Props Reference
| Prop Name | Type | What It Does | Default Value |
|-----------|------|--------------|---------------|
| Canvas Properties | | | |
| width | string \| number | Sets the canvas width. Use "100%" for responsive layouts | "100%" |
| height | string \| number | Sets the canvas height. Numbers are treated as pixels | 600 |
| className | string | Custom CSS class names for the canvas container | undefined |
| style | React.CSSProperties | Inline styles for the canvas container | {} |
| disabled | boolean | Disables all interactions when true | false |
| Grid Settings | | | |
| enableGrid | boolean | Enables grid snapping for precise element positioning | false |
| showGrid | boolean | Shows or hides the grid background pattern | false |
| gridSize | number | Size of each grid cell in pixels | 20 |
| gridColor | string | Color of the grid lines (any valid CSS color) | "#e5e7eb" |
| gridOpacity | number | Transparency of grid lines (0-1) | 1 |
| Element Management | | | |
| elements | DraggableElement[] | Array of elements to display on the canvas | [] |
| selectedElement | DraggableElement \| null | Currently selected element | null |
| onUpdateElementPosition | (id: string, position: {x: number, y: number}) => void | Called when element is moved | undefined |
| onUpdateElementSize | (id: string, size: {width: number, height: number}, position?: {x: number, y: number}) => void | Called when element is resized | undefined |
| onSelectElement | (id: string \| null) => void | Called when element is selected or deselected | undefined |
| Interaction | | | |
| onCanvasClick | (e: React.MouseEvent) => void | Called when clicking empty canvas space | undefined |
| constrainToCanvas | boolean | Prevents elements from leaving canvas boundaries | true |
| Visual Effects | | | |
| draggableProps.enableDragShadow | boolean | Shows shadow while dragging elements | false |
| draggableProps.enableResizeShadow | boolean | Shows shadow while resizing elements | false |
| draggableProps.dragScale | number | Scale factor when dragging (1.0 = no scale) | 1.02 |
| draggableProps.resizeScale | number | Scale factor when resizing (1.0 = no scale) | 1.01 |
| draggableProps.dragShadow | string | Custom shadow CSS for dragging | undefined |
| draggableProps.resizeShadow | string | Custom shadow CSS for resizing | undefined |
🎯 useDraggableState Hook
This hook is your state management powerhouse! It handles all the complex logic so you can focus on building amazing user experiences.
const {
elements, // Current elements
selectedElement, // Selected element
updateElementPosition, // Update element position
updateElementSize, // Update element size
addElement, // Add new element
removeElement, // Remove element
selectElement, // Select element
bringToFront, // Bring to front
sendToBack, // Send to back
duplicateElement, // Duplicate element
clearSelection // Clear selection
} = useDraggableState({
initialElements: [...],
onElementsChange: (elements) => console.log('Elements changed:', elements)
});📋 useDraggableState Reference
| Return Value | Type | What It Does | How to Use |
|--------------|------|--------------|------------|
| State Values | | | |
| elements | DraggableElement[] | Current array of all elements | elements.map(el => ...) |
| selectedElement | DraggableElement \| null | Currently selected element | selectedElement?.id |
| Element Actions | | | |
| addElement | (element: DraggableElement) => void | Adds a new element to the canvas | addElement({...}) |
| removeElement | (id: string) => void | Removes an element by ID | removeElement('box-1') |
| updateElementPosition | (id: string, position: {x: number, y: number}) => void | Changes element position | updateElementPosition('box-1', {x: 100, y: 200}) |
| updateElementSize | (id: string, size: {width: number, height: number}, position?: {x: number, y: number}) => void | Changes element size | updateElementSize('box-1', {width: 150, height: 100}) |
| Selection Management | | | |
| selectElement | (id: string \| null) => void | Selects an element or clears selection | selectElement('box-1') or selectElement(null) |
| clearSelection | () => void | Deselects current element | clearSelection() |
| Layer Management | | | |
| bringToFront | (id: string) => void | Moves element to top layer | bringToFront('box-1') |
| sendToBack | (id: string) => void | Moves element to bottom layer | sendToBack('box-1') |
| duplicateElement | (id: string) => void | Creates a copy of an element | duplicateElement('box-1') |
📋 Hook Configuration Options
| Option | Type | What It Does | Default Value |
|--------|------|--------------|---------------|
| initialElements | DraggableElement[] | Elements to show when canvas first loads | [] |
| onElementsChange | (elements: DraggableElement[]) => void | Callback when any element changes | undefined |
📋 DraggableElement Type
| Property | Type | What It Is | Example |
|----------|------|------------|---------|
| id | string | Unique identifier for the element | 'box-1' |
| type | string | Element type for custom rendering | 'rectangle' |
| position | {x: number, y: number} | Element position in pixels | {x: 100, y: 50} |
| size | {width: number, height: number} | Element dimensions in pixels | {width: 200, height: 150} |
| zIndex | number | Layer order (higher = on top) | 1 |
| properties | Record<string, any> | Custom properties for your rendering | {fill: '#3b82f6', opacity: 0.8} |
| constraints | ElementConstraints | Size and behavior restrictions | {minSize: {width: 50, height: 50}} |
🎨 Children Render Function
The children prop is where your creativity shines! It's a function that renders each element on your canvas.
<Canvas {...props}>
{(element, isSelected) => {
// element: The element data
// isSelected: Boolean indicating if this element is selected
return <YourCustomComponent element={element} isSelected={isSelected} />
}}
</Canvas>📋 Render Function Parameters
| Parameter | Type | What It Gives You | Usage Example |
|-----------|------|-------------------|---------------|
| element | DraggableElement | All data about the current element | element.properties?.fill |
| isSelected | boolean | Whether this element is currently selected | isSelected && 'ring-2 ring-blue-500' |
🎨 Customization Guide
Custom Resize Handles
import { ResizeHandle } from 'dragstack';
const CustomHandle = (props) => (
<ResizeHandle
{...props}
size={12}
color="#3b82f6"
style={{
backgroundColor: '#3b82f6',
border: '2px solid white',
borderRadius: '50%'
}}
/>
);Grid Customization
<Canvas
enableGrid={true}
showGrid={true}
gridSize={30} // Grid cell size
gridColor="#cbd5e1" // Grid color
gridOpacity={0.3} // Grid transparency
constrainToCanvas={true}
>
{children}
</Canvas>Custom Shadows and Effects
<Canvas
draggableProps={{
enableDragShadow: true,
enableResizeShadow: true,
dragShadow: "0 25px 50px rgba(0, 0, 0, 0.25)",
resizeShadow: "0 10px 25px rgba(0, 0, 0, 0.15)",
dragScale: 1.05,
resizeScale: 1.02
}}
>
{children}
</Canvas>📱 Browser Support
| Browser | Version | Status | |---------|---------|--------| | Chrome | 90+ | ✅ Full Support | | Firefox | 88+ | ✅ Full Support | | Safari | 14+ | ✅ Full Support | | Edge | 90+ | ✅ Full Support |
⚙️ Requirements & Compatibility
React Version Support
- ✅ React 18.x - Fully supported and recommended
- ❌ React 19.x - Not yet supported (breaking changes in hooks API)
- ❌ React < 18 - Not supported
Required Dependencies
- React 18.x - Core dependency
- React DOM 18.x - Core dependency
- Tailwind CSS - Required for component styling
- TypeScript 5.x - Recommended for type safety
Setup for New Projects
If you're starting a new project, here's the complete setup:
# Create React app with Vite
npm create vite@latest my-app -- --template react-ts
cd my-app
# Ensure React 18
npm install react@^18.0.0 react-dom@^18.0.0
# Install and configure Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Configure tailwind.config.js
# Add: content: ["./src/**/*.{js,ts,jsx,tsx}"]
# Add Tailwind directives to your CSS
# @tailwind base; @tailwind components; @tailwind utilities;
# Install Dragstack
npm install dragstack
# Start development
npm run dev🚀 Performance
Canvas Editor is optimized for performance:
- ⚡ GPU Accelerated animations using CSS transforms
- 🎯 Smart Re-rendering with React optimization
- 📦 Tiny Bundle Size - tree-shakable and minimal dependencies
- 🔄 60 FPS smooth interactions even with 100+ elements
- 💾 Memory Efficient - no memory leaks or excessive allocations
Performance Tips
- Use
React.memofor custom child components - Enable grid snapping for smoother movements
- Use constraints to prevent unnecessary calculations
- Avoid inline functions in render props
🤝 Contributing
We love contributions! 🎉
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Development Setup
git clone https://github.com/your-username/canvas-editor.git
cd canvas-editor
npm install
npm run dev
npm test📄 License
MIT © Canvas Editor Team
🙏 Acknowledgments
Built with ❤️ by the Canvas Editor team, inspired by modern design tools and the amazing React community.
📚 Need Help?
⭐ Star this repo if you find it helpful! It helps us know we're making a difference.
