Next.js ImageEditor Integration Guide
Complete guide for integrating the@distralabs/media-editor ImageEditor component into your Next.js application.
Table of Contents
- Prerequisites
- Installation
- Basic Setup
- ImageEditor Interface
- Step-by-Step Integration
- Theme Customization
- Callbacks and Event Handling
- Complete Example
Prerequisites
- Node.js 16.x or higher
- Next.js 14.x or 15.x
- React 18.2.0 (SDK requires React 18, not React 19)
Installation
1. Install the SDK
Copy
npm install @distralabs/media-editor
2. Install Required Dependencies
Copy
npm install framer-motion lucide-react clsx tailwind-merge
3. Configure Next.js for React 18
If you’re using Next.js 15+ (which requires React 19), you need to downgrade to Next.js 14: package.json:Copy
{
"dependencies": {
"@distralabs/media-editor": "^1.1.9",
"next": "14.2.15",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.15"
},
"overrides": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
}
Basic Setup
1. Import SDK Styles
Add the SDK CSS to your global styles file: app/globals.css:Copy
@import "tailwindcss";
/* Import media-editor SDK styles */
@import "@distralabs/media-editor/dist/index.css";
/* Your other styles */
2. Configure Next.js
next.config.js:Copy
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
// Disable node modules that don't work in browser
config.resolve.alias = {
...config.resolve.alias,
canvas: false,
fs: false,
};
return config;
},
};
module.exports = nextConfig;
3. Use Dynamic Import (Avoid SSR)
The SDK uses browser-only APIs, so you must use dynamic imports:Copy
'use client';
import dynamic from 'next/dynamic';
const ImageEditor = dynamic(
() => import('@distralabs/media-editor').then(mod => ({ default: mod.ImageEditor })),
{ ssr: false }
);
ImageEditor Interface
Core Props
Copy
interface ImageEditorProps {
// Required
licenseKey: string; // Your SDK license key (JWT)
onClose: () => void; // Called when user closes editor
// Optional
apiUrl?: string; // Override license validation API URL
files?: File; // Initial image file to load
callback?: (result: CallbackProps, extras?: EditorExtras) => void;
theme?: Record<string, string>; // Custom theme colors
showThemeCreator?: boolean; // Show theme customization UI
headless?: boolean; // Enable programmatic mode
brands?: BrandDetails[]; // Brand presets
defaultTemplate?: Template; // Load template on start
// Advanced
onExport?: (templateId: string, brandResults: any, updatedImage: string, thumbUri: string) => void;
onSaveTemplate?: (props: { brandId: string; template: any }) => Promise<void>;
onGetTemplates?: (brandIdList?: string[]) => Promise<void | { success: { data: any } }>;
createAutomatedContent?: CreateAutomatedContent;
calls?: CallItem[]; // Headless mode: functions to execute
}
Callback Types
Copy
interface CallbackProps {
base64: string; // Exported image as base64 data URL
width: number; // Canvas width
height: number; // Canvas height
template?: any; // Scene data if template was used
}
interface EditorExtras {
thumbnail?: string; // Thumbnail preview
format?: string; // Image format (png, jpg, etc)
}
Step-by-Step Integration
Step 1: Create Your Page Component
app/studio/image/page.tsx:Copy
'use client';
import { useState, useCallback, useRef } from 'react';
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
// Dynamic import to avoid SSR
const ImageEditor = dynamic(
() => import('@distralabs/media-editor').then(mod => ({ default: mod.ImageEditor })),
{ ssr: false }
);
export default function ImageStudioPage() {
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement>(null);
// State management
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [showEditor, setShowEditor] = useState(false);
const [exportedImage, setExportedImage] = useState<string | null>(null);
// Continue to Step 2...
}
Step 2: Implement File Selection
Copy
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file && file.type.startsWith('image/')) {
setSelectedFile(file);
setShowEditor(true);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const file = e.dataTransfer.files?.[0];
if (file && file.type.startsWith('image/')) {
setSelectedFile(file);
setShowEditor(true);
}
};
Step 3: Implement Callbacks
Copy
const handleExport = useCallback((result: any, extras?: any) => {
console.log('Export result:', result, extras);
if (result.base64) {
setExportedImage(result.base64);
setShowEditor(false);
// Show success modal or download
}
}, []);
const handleClose = () => {
setShowEditor(false);
setSelectedFile(null);
};
const handleDownload = () => {
if (exportedImage) {
const link = document.createElement('a');
link.href = exportedImage;
link.download = `edited-image-${Date.now()}.png`;
link.click();
}
};
Step 4: Render the Editor
Copy
return (
<div className="min-h-screen">
{/* Upload UI */}
{!showEditor && (
<div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
<button onClick={() => fileInputRef.current?.click()}>
Choose Image
</button>
</div>
)}
{/* Image Editor */}
{showEditor && selectedFile && (
<div className="fixed inset-0 z-40">
<ImageEditor
licenseKey="YOUR_LICENSE_KEY_HERE"
apiUrl="https://your-api.com/social"
files={selectedFile}
onClose={handleClose}
callback={handleExport}
theme={customTheme}
showThemeCreator={false}
/>
</div>
)}
</div>
);
Theme Customization
Creating a Custom Theme
Copy
const customTheme = {
// Backgrounds
'background.primary': '#0f172a', // Main background
'background.secondary': '#1e293b', // Secondary panels
'background.tertiary': '#334155', // Tertiary elements
// Text
'text.primary': '#ffffff', // Primary text
'text.secondary': '#cbd5e0', // Secondary text
// Brand colors
'accent.primary': '#3b82f6', // Primary actions
'accent.secondary': '#06b6d4', // Secondary actions
'accent.hover': '#60a5fa', // Hover states
// Borders
'border.default': '#334155',
'border.subtle': '#1e293b',
// Components
'button.primary': '#3b82f6',
'input.background': '#1e293b',
'toolbar.background': '#0f172a',
'canvas.background': '#1a202c',
};
Applying the Theme
Copy
<ImageEditor
licenseKey={licenseKey}
files={selectedFile}
onClose={handleClose}
callback={handleExport}
theme={customTheme}
showThemeCreator={false} // Hide theme customization UI
/>
Callbacks and Event Handling
Export Callback
Called when user exports the edited image:Copy
const handleExport = useCallback((result: CallbackProps, extras?: EditorExtras) => {
console.log('Exported image:', {
base64: result.base64, // Data URL of image
width: result.width, // Canvas width
height: result.height, // Canvas height
thumbnail: extras?.thumbnail // Optional thumbnail
});
// Download the image
const link = document.createElement('a');
link.href = result.base64;
link.download = 'edited-image.png';
link.click();
// Or upload to server
fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({ image: result.base64 }),
headers: { 'Content-Type': 'application/json' }
});
}, []);
Close Callback
Called when user clicks close/cancel:Copy
const handleClose = () => {
// Confirm before closing if changes were made
const confirmed = window.confirm('Discard changes?');
if (confirmed) {
setShowEditor(false);
setSelectedFile(null);
}
};
Template Callbacks
Save and load custom templates:Copy
const handleSaveTemplate = async ({ brandId, template }: any) => {
const response = await fetch('/api/templates', {
method: 'POST',
body: JSON.stringify({ brandId, template }),
headers: { 'Content-Type': 'application/json' }
});
return response.json();
};
const handleGetTemplates = async (brandIds?: string[]) => {
const response = await fetch('/api/templates?' + new URLSearchParams({
brandIds: brandIds?.join(',') || ''
}));
return response.json();
};
<ImageEditor
onSaveTemplate={handleSaveTemplate}
onGetTemplates={handleGetTemplates}
// ... other props
/>
Complete Example
app/studio/image/page.tsx:Copy
'use client';
import { useState, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Upload, Download, X } from 'lucide-react';
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
const ImageEditor = dynamic(
() => import('@distralabs/media-editor').then(mod => ({ default: mod.ImageEditor })),
{ ssr: false }
);
const customTheme = {
'background.primary': '#0f172a',
'background.secondary': '#1e293b',
'background.tertiary': '#334155',
'text.primary': '#ffffff',
'text.secondary': '#cbd5e0',
'accent.primary': '#3b82f6',
'accent.secondary': '#06b6d4',
'accent.hover': '#60a5fa',
'border.default': '#334155',
'border.subtle': '#1e293b',
};
export default function ImageStudioPage() {
const router = useRouter();
const fileInputRef = useRef<HTMLInputElement>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [showEditor, setShowEditor] = useState(false);
const [exportedImage, setExportedImage] = useState<string | null>(null);
const [showExportModal, setShowExportModal] = useState(false);
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file && file.type.startsWith('image/')) {
setSelectedFile(file);
setShowEditor(true);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const file = e.dataTransfer.files?.[0];
if (file && file.type.startsWith('image/')) {
setSelectedFile(file);
setShowEditor(true);
}
};
const handleExport = useCallback((result: any, extras?: any) => {
console.log('Export result:', result, extras);
if (result.base64) {
setExportedImage(result.base64);
setShowExportModal(true);
setShowEditor(false);
}
}, []);
const handleClose = () => {
setShowEditor(false);
setSelectedFile(null);
};
const handleDownload = () => {
if (exportedImage) {
const link = document.createElement('a');
link.href = exportedImage;
link.download = `edited-image-${Date.now()}.png`;
link.click();
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
{/* Upload Interface */}
<AnimatePresence>
{!showEditor && !showExportModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="flex flex-col items-center justify-center min-h-screen p-6"
>
<div
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
onClick={() => fileInputRef.current?.click()}
className="relative w-full max-w-2xl cursor-pointer"
>
<div className="bg-white/10 backdrop-blur-md border-2 border-dashed border-white/30 rounded-3xl p-16 text-center hover:border-purple-400 transition-all">
<Upload className="w-20 h-20 mx-auto mb-6 text-purple-400" />
<h3 className="text-2xl font-bold text-white mb-3">
Drop your image here
</h3>
<p className="text-slate-300 mb-6">
or click to browse your files
</p>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
/>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Image Editor */}
{showEditor && selectedFile && (
<div className="fixed inset-0 z-40">
<ImageEditor
licenseKey="YOUR_LICENSE_KEY_HERE"
apiUrl="https://localhost:3030/social"
files={selectedFile}
onClose={handleClose}
callback={handleExport}
theme={customTheme}
showThemeCreator={false}
/>
</div>
)}
{/* Export Modal */}
<AnimatePresence>
{showExportModal && exportedImage && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center p-6 bg-black/80"
onClick={() => setShowExportModal(false)}
>
<motion.div
initial={{ scale: 0.8, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.8, y: 20 }}
onClick={(e) => e.stopPropagation()}
className="bg-slate-800 rounded-2xl p-8 max-w-4xl w-full"
>
<div className="flex justify-between items-center mb-6">
<h2 className="text-3xl font-bold text-white">
Your Creation is Ready!
</h2>
<button
onClick={() => setShowExportModal(false)}
className="p-2 hover:bg-white/10 rounded-lg"
>
<X className="w-6 h-6 text-white" />
</button>
</div>
<div className="mb-6 rounded-xl overflow-hidden bg-slate-900">
<img src={exportedImage} alt="Edited" className="w-full h-auto" />
</div>
<div className="flex gap-4">
<button
onClick={handleDownload}
className="flex-1 flex items-center justify-center gap-2 px-6 py-4 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl text-white font-bold"
>
<Download className="w-5 h-5" />
Download Image
</button>
<button
onClick={() => {
setShowExportModal(false);
setSelectedFile(null);
setExportedImage(null);
}}
className="flex-1 px-6 py-4 bg-white/10 rounded-xl text-white font-bold"
>
Create Another
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Next Steps
- See FAQ.md for common issues and troubleshooting
- See NEXTJS_VIDEOEDITOR_INTEGRATION.md for video editing
- Check the included example project for a complete working implementation
Support
For issues or questions, please visit:- Support Portal: Contact your account manager or [email protected]
- Documentation: https://docs.distralabs.com