Integrations
Integrate Imigi into any web framework or vanilla JavaScript project.
Vanilla JavaScript / HTML
The simplest way to use Imigi is with plain HTML and a <script> tag. No build step or framework required.
HTML Structure
Create a container element with explicit dimensions. Imigi renders the editor inside this element.
<div id="editor-container" style="width: 100%; height: 600px;"></div>
Script Tag Setup
Load Imigi via the UMD bundle from a CDN or a local file:
<script src="https://unpkg.com/imigi/dist-umd/imigi.umd.js"></script>
Configuration
Initialize the editor with your desired configuration. The Imigi object is available globally on window.Imigi when using the UMD bundle.
const editor = await Imigi.init({
selector: '#editor-container',
image: 'photo.jpg',
theme: 'dark',
tools: ['filter', 'crop', 'draw', 'text', 'shapes'],
export: {
format: 'png',
quality: 0.92,
},
});
Handling Save
Use the onSave callback to receive the edited image data when the user clicks Save:
const editor = await Imigi.init({
selector: '#editor-container',
image: 'photo.jpg',
onSave(data, filename, format) {
// Download the edited image
const link = document.createElement('a');
link.href = data;
link.download = filename;
link.click();
},
});
Full Working Example
Here is a complete, self-contained HTML page you can save and open in any browser:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imigi Editor</title>
<style>
body { margin: 0; font-family: sans-serif; }
#editor-container { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="editor-container"></div>
<script src="https://unpkg.com/imigi/dist-umd/imigi.umd.js"></script>
<script>
Imigi.init({
selector: '#editor-container',
image: 'https://picsum.photos/800/600',
theme: 'dark',
tools: ['filter', 'crop', 'draw', 'text', 'shapes', 'stickers'],
onSave(data, filename) {
const link = document.createElement('a');
link.href = data;
link.download = filename;
link.click();
},
});
</script>
</body>
</html>
React
Imigi integrates naturally with React using useEffect and useRef. Since Imigi manages its own DOM, it should be initialized inside an effect and cleaned up on unmount.
Installation
npm install imigi
yarn add imigi
pnpm add imigi
Basic Component
Create a wrapper component that initializes Imigi on mount and destroys it on unmount:
import { useEffect, useRef } from 'react';
import { Imigi } from 'imigi';
function ImageEditor({ imageUrl, onSave }) {
const containerRef = useRef(null);
const editorRef = useRef(null);
useEffect(() => {
let cancelled = false;
async function initEditor() {
const editor = await Imigi.init({
container: containerRef.current,
image: imageUrl,
onSave: onSave,
});
if (cancelled) {
editor.destroy();
return;
}
editorRef.current = editor;
}
initEditor();
return () => {
cancelled = true;
editorRef.current?.destroy();
editorRef.current = null;
};
}, [imageUrl]);
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
}
export default ImageEditor;
Handling Props Changes
When the imageUrl prop changes, the effect re-runs, destroying the old editor and creating a new one. If you want to change the image without reinitializing, use the loadImage method:
useEffect(() => {
if (editorRef.current && imageUrl) {
editorRef.current.loadImage(imageUrl);
}
}, [imageUrl]);
Cleanup on Unmount
Always call editor.destroy() in the cleanup function of useEffect. This removes DOM elements, event listeners, and frees canvas memory.
destroy() will lead to memory leaks, especially if the component is mounted and unmounted frequently (e.g., in a modal).
Full Component Example (TypeScript)
import React, { useEffect, useRef, useCallback } from 'react';
import { Imigi, ImigiInstance, ImigiConfig } from 'imigi';
interface ImageEditorProps {
imageUrl: string;
tools?: ImigiConfig['tools'];
theme?: 'light' | 'dark';
onSave?: (data: string, filename: string, format: string) => void;
onClose?: () => void;
}
const ImageEditor: React.FC<ImageEditorProps> = ({
imageUrl,
tools = ['filter', 'crop', 'draw', 'text', 'shapes'],
theme = 'dark',
onSave,
onClose,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<ImigiInstance | null>(null);
const handleSave = useCallback(
(data: string, filename: string, format: string) => {
onSave?.(data, filename, format);
},
[onSave]
);
useEffect(() => {
if (!containerRef.current) return;
let cancelled = false;
Imigi.init({
container: containerRef.current,
image: imageUrl,
tools,
theme,
onSave: handleSave,
onClose,
}).then((editor) => {
if (cancelled) {
editor.destroy();
return;
}
editorRef.current = editor;
});
return () => {
cancelled = true;
editorRef.current?.destroy();
editorRef.current = null;
};
}, [imageUrl, theme, tools, handleSave, onClose]);
return (
<div
ref={containerRef}
style={{ width: '100%', height: '600px' }}
/>
);
};
export default ImageEditor;
- Use
useCallbackfor theonSavehandler to avoid unnecessary re-initializations. - Avoid placing the editor inside a component that re-renders frequently. Wrap it with
React.memoif needed. - If you need to access the editor instance imperatively, expose it via
useImperativeHandlewithforwardRef. - When using Strict Mode in development, the editor will initialize twice. The cleanup function handles this correctly.
Vue 3 (Composition API)
Vue 3's Composition API with setup, onMounted, and ref provides a clean way to integrate Imigi.
Installation
npm install imigi
Component Setup
Use onMounted to initialize the editor and onUnmounted for cleanup. Use watch to react to prop changes.
Full SFC Example
<template>
<div ref="containerRef" class="imigi-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { Imigi, ImigiInstance } from 'imigi';
const props = defineProps<{
imageUrl: string;
theme?: 'light' | 'dark';
}>();
const emit = defineEmits<{
(e: 'save', data: string, filename: string, format: string): void;
(e: 'close'): void;
}>();
const containerRef = ref<HTMLDivElement | null>(null);
let editor: ImigiInstance | null = null;
onMounted(async () => {
if (!containerRef.value) return;
editor = await Imigi.init({
container: containerRef.value,
image: props.imageUrl,
theme: props.theme ?? 'dark',
onSave(data, filename, format) {
emit('save', data, filename, format);
},
onClose() {
emit('close');
},
});
});
// React to image URL changes
watch(
() => props.imageUrl,
(newUrl) => {
if (editor && newUrl) {
editor.loadImage(newUrl);
}
}
);
// React to theme changes
watch(
() => props.theme,
(newTheme) => {
if (editor && newTheme) {
editor.setTheme(newTheme);
}
}
);
onUnmounted(() => {
editor?.destroy();
editor = null;
});
</script>
<style scoped>
.imigi-container {
width: 100%;
height: 600px;
}
</style>
- Do not wrap the editor instance in
ref()orreactive(). The Imigi instance is not a plain object and should not be made reactive. - Use
watchto respond to prop changes and call editor methods directly instead of reinitializing. - The template ref (
containerRef) is only available afteronMounted, so always initialize inside the mounted hook.
Vue 2 (Options API)
For Vue 2 projects using the Options API, use the mounted and beforeDestroy lifecycle hooks:
<template>
<div ref="editorContainer" class="imigi-container"></div>
</template>
<script>
import { Imigi } from 'imigi';
export default {
name: 'ImageEditor',
props: {
imageUrl: { type: String, required: true },
theme: { type: String, default: 'dark' },
},
data() {
return { editor: null };
},
async mounted() {
this.editor = await Imigi.init({
container: this.$refs.editorContainer,
image: this.imageUrl,
theme: this.theme,
onSave: (data, filename, format) => {
this.$emit('save', data, filename, format);
},
});
},
watch: {
imageUrl(newUrl) {
this.editor?.loadImage(newUrl);
},
},
beforeDestroy() {
this.editor?.destroy();
this.editor = null;
},
};
</script>
Angular
In Angular, use ngAfterViewInit to initialize the editor after the view is rendered, and ngOnDestroy for cleanup.
Installation
npm install imigi
Full Component Example
import {
Component,
Input,
Output,
EventEmitter,
ViewChild,
ElementRef,
AfterViewInit,
OnDestroy,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { Imigi, ImigiInstance } from 'imigi';
@Component({
selector: 'app-image-editor',
template: `<div #editorContainer class="imigi-container"></div>`,
styles: [`.imigi-container { width: 100%; height: 600px; }`],
})
export class ImageEditorComponent implements AfterViewInit, OnDestroy, OnChanges {
@Input() imageUrl!: string;
@Input() theme: 'light' | 'dark' = 'dark';
@Output() save = new EventEmitter<{
data: string;
filename: string;
format: string;
}>();
@ViewChild('editorContainer', { static: true })
containerRef!: ElementRef<HTMLDivElement>;
private editor: ImigiInstance | null = null;
async ngAfterViewInit() {
this.editor = await Imigi.init({
container: this.containerRef.nativeElement,
image: this.imageUrl,
theme: this.theme,
onSave: (data, filename, format) => {
this.save.emit({ data, filename, format });
},
});
}
ngOnChanges(changes: SimpleChanges) {
if (!this.editor) return;
if (changes['imageUrl'] && !changes['imageUrl'].firstChange) {
this.editor.loadImage(changes['imageUrl'].currentValue);
}
if (changes['theme'] && !changes['theme'].firstChange) {
this.editor.setTheme(changes['theme'].currentValue);
}
}
ngOnDestroy() {
this.editor?.destroy();
this.editor = null;
}
}
Service Pattern for Shared Instance
If multiple components need access to the same editor instance, create a shared Angular service:
import { Injectable } from '@angular/core';
import { Imigi, ImigiInstance, ImigiConfig } from 'imigi';
@Injectable({ providedIn: 'root' })
export class ImageEditorService {
private instance: ImigiInstance | null = null;
async create(config: ImigiConfig): Promise<ImigiInstance> {
this.destroy(); // Clean up any previous instance
this.instance = await Imigi.init(config);
return this.instance;
}
getInstance(): ImigiInstance | null {
return this.instance;
}
destroy() {
this.instance?.destroy();
this.instance = null;
}
}
- Use
static: trueon@ViewChildif you need the reference inngOnInit, orstatic: false(default) forngAfterViewInit. - If the editor container is inside an
*ngIf, usengAfterViewInitwithstatic: falseto ensure the element exists. - Angular's change detection does not affect Imigi since it manages its own internal state via Fabric.js.
Next.js
Imigi requires access to the DOM and uses Fabric.js which depends on the HTML5 Canvas API. This means it must only run on the client side in Next.js.
Dynamic Import (No SSR)
Use next/dynamic to load the editor component only on the client, preventing server-side rendering errors:
'use client';
import dynamic from 'next/dynamic';
const ImageEditor = dynamic(
() => import('@/components/ImageEditor'),
{ ssr: false }
);
export default function EditorPage() {
return (
<div>
<h1>Edit Image</h1>
<ImageEditor
imageUrl="/photos/sample.jpg"
onSave={(data) => console.log('Saved', data)}
/>
</div>
);
}
Client-Only Component
Create a dedicated client component that wraps the Imigi initialization logic:
'use client';
import { useEffect, useRef } from 'react';
import type { ImigiInstance } from 'imigi';
interface Props {
imageUrl: string;
onSave?: (data: string, filename: string) => void;
}
export default function ImageEditor({ imageUrl, onSave }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<ImigiInstance | null>(null);
useEffect(() => {
let cancelled = false;
// Dynamic import to avoid SSR issues
import('imigi').then(async ({ Imigi }) => {
if (cancelled || !containerRef.current) return;
const editor = await Imigi.init({
container: containerRef.current,
image: imageUrl,
onSave: onSave,
});
if (cancelled) {
editor.destroy();
return;
}
editorRef.current = editor;
});
return () => {
cancelled = true;
editorRef.current?.destroy();
editorRef.current = null;
};
}, [imageUrl, onSave]);
return <div ref={containerRef} style={{ width: '100%', height: '600px' }} />;
}
If you see hydration mismatch errors, make sure the component is loaded with ssr: false via next/dynamic. Imigi modifies the DOM directly, which conflicts with Next.js hydration if the component renders on the server.
- Always use
'use client'at the top of the editor component file. - Use
next/dynamicwithssr: falsewhen importing the editor in a page or layout. - For the App Router, place the editor in a client component and import it into server components with dynamic import.
- If using the Pages Router, the same
next/dynamicapproach works ingetStaticPropsorgetServerSidePropspages.
Nuxt 3
Nuxt 3 uses server-side rendering by default, so Imigi must be wrapped in a client-only context.
Client-Only Plugin
Create a Nuxt plugin that registers Imigi on the client side only:
import { Imigi } from 'imigi';
export default defineNuxtPlugin(() => {
return {
provide: {
imigi: Imigi,
},
};
});
Component with ClientOnly
Wrap the editor component in Nuxt's <ClientOnly> component to prevent SSR:
<template>
<div>
<h1>Image Editor</h1>
<ClientOnly>
<ImageEditor image-url="/photos/sample.jpg" @save="handleSave" />
<template #fallback>
<p>Loading editor...</p>
</template>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
function handleSave(data: string, filename: string) {
console.log('Saved:', filename);
}
</script>
Full Editor Component
<template>
<div ref="containerRef" class="imigi-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const props = defineProps<{
imageUrl: string;
}>();
const emit = defineEmits<{
(e: 'save', data: string, filename: string): void;
}>();
const containerRef = ref<HTMLDivElement | null>(null);
const { $imigi } = useNuxtApp();
let editor: any = null;
onMounted(async () => {
if (!containerRef.value) return;
editor = await $imigi.init({
container: containerRef.value,
image: props.imageUrl,
onSave(data: string, filename: string) {
emit('save', data, filename);
},
});
});
onUnmounted(() => {
editor?.destroy();
editor = null;
});
</script>
<style scoped>
.imigi-container {
width: 100%;
height: 600px;
}
</style>
- The
.client.tssuffix on the plugin file ensures it only runs in the browser. - Always wrap Imigi components with
<ClientOnly>to prevent SSR hydration errors. - Use the
#fallbackslot to show a loading placeholder while the editor initializes.
Svelte
Svelte's onMount and reactive declarations make Imigi integration straightforward.
Full Component Example
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { Imigi } from 'imigi';
import type { ImigiInstance } from 'imigi';
import { createEventDispatcher } from 'svelte';
export let imageUrl: string;
export let theme: 'light' | 'dark' = 'dark';
const dispatch = createEventDispatcher();
let container: HTMLDivElement;
let editor: ImigiInstance | null = null;
onMount(async () => {
editor = await Imigi.init({
container,
image: imageUrl,
theme,
onSave(data, filename, format) {
dispatch('save', { data, filename, format });
},
});
});
// Reactive: reload image when imageUrl changes
$: if (editor && imageUrl) {
editor.loadImage(imageUrl);
}
// Reactive: update theme when it changes
$: if (editor && theme) {
editor.setTheme(theme);
}
onDestroy(() => {
editor?.destroy();
editor = null;
});
</script>
<div
bind:this={container}
class="imigi-container"
></div>
<style>
.imigi-container {
width: 100%;
height: 600px;
}
</style>
Usage in a parent component:
<script>
import ImageEditor from '$lib/ImageEditor.svelte';
function handleSave(event) {
const { data, filename } = event.detail;
console.log('Saved:', filename);
}
</script>
<ImageEditor
imageUrl="/photos/sample.jpg"
theme="dark"
on:save={handleSave}
/>
- Use
bind:thisto get a direct reference to the container DOM element. - Svelte's reactive declarations (
$:) work well for responding to prop changes and calling editor methods. - For SvelteKit with SSR, use the
{#if browser}check oronMount(which only runs in the browser) to avoid SSR issues.
Web Components
You can wrap Imigi in a standard Web Component (Custom Element) for use in any framework or vanilla HTML. This approach provides maximum portability.
import { Imigi } from 'imigi';
class ImigiEditor extends HTMLElement {
static get observedAttributes() {
return ['src', 'theme'];
}
constructor() {
super();
this.editor = null;
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host { display: block; width: 100%; height: 600px; }
.container { width: 100%; height: 100%; }
</style>
<div class="container"></div>
`;
}
async connectedCallback() {
const container = this.shadowRoot.querySelector('.container');
this.editor = await Imigi.init({
container,
image: this.getAttribute('src') || '',
theme: this.getAttribute('theme') || 'dark',
onSave: (data, filename, format) => {
this.dispatchEvent(
new CustomEvent('imigi-save', {
detail: { data, filename, format },
bubbles: true,
composed: true,
})
);
},
});
}
disconnectedCallback() {
this.editor?.destroy();
this.editor = null;
}
attributeChangedCallback(name, oldValue, newValue) {
if (!this.editor || oldValue === newValue) return;
if (name === 'src') this.editor.loadImage(newValue);
if (name === 'theme') this.editor.setTheme(newValue);
}
}
customElements.define('imigi-editor', ImigiEditor);
Then use it in any HTML:
<imigi-editor src="photo.jpg" theme="dark"></imigi-editor>
<script>
document.querySelector('imigi-editor')
.addEventListener('imigi-save', (e) => {
console.log('Saved:', e.detail.filename);
});
</script>
TypeScript Support
Imigi ships with full TypeScript type definitions out of the box. No additional @types packages are needed.
Type Imports
import {
Imigi,
ImigiInstance,
ImigiConfig,
ImigiTool,
ImigiTheme,
ImigiExportFormat,
ImigiEvent,
} from 'imigi';
Type-Safe Configuration
import { Imigi, ImigiConfig } from 'imigi';
const config: ImigiConfig = {
selector: '#editor',
image: 'photo.jpg',
theme: 'dark',
tools: ['filter', 'crop', 'draw', 'text', 'shapes'],
export: {
format: 'png', // Type-checked: 'png' | 'jpeg' | 'webp' | 'svg' | 'json'
quality: 0.92, // Type-checked: number between 0 and 1
filename: 'edited', // Type-checked: string
},
onSave(data: string, filename: string, format: string) {
// Fully typed callback parameters
console.log(data, filename, format);
},
};
const editor = await Imigi.init(config);
Full Typed Example
import { Imigi, ImigiInstance, ImigiConfig, ImigiTool } from 'imigi';
async function createEditor(
container: HTMLElement,
imageUrl: string,
tools: ImigiTool[] = ['filter', 'crop', 'draw']
): Promise<ImigiInstance> {
const config: ImigiConfig = {
container,
image: imageUrl,
tools,
theme: 'dark',
onSave(data: string, filename: string) {
downloadImage(data, filename);
},
};
return Imigi.init(config);
}
function downloadImage(dataUrl: string, filename: string): void {
const link = document.createElement('a');
link.href = dataUrl;
link.download = filename;
link.click();
}
// Usage
const el = document.getElementById('editor')!;
const editor: ImigiInstance = await createEditor(el, 'photo.jpg');
// Instance methods are fully typed
await editor.loadImage('new-photo.jpg');
editor.setTheme('light');
editor.destroy();
Common Integration Patterns
These patterns apply to all frameworks and vanilla JavaScript. Adapt the code to your specific environment.
Editor as Modal
A common pattern is to open the editor in a modal or overlay triggered by a user action (e.g., clicking an "Edit" button on a thumbnail).
let editor = null;
async function openEditor(imageUrl) {
// Show the modal container
const modal = document.getElementById('editor-modal');
modal.style.display = 'flex';
editor = await Imigi.init({
selector: '#editor-modal .editor-area',
image: imageUrl,
ui: { mode: 'inline' },
onSave(data, filename) {
// Update the thumbnail or upload the image
document.getElementById('thumbnail').src = data;
closeEditor();
},
onClose() {
closeEditor();
},
});
}
function closeEditor() {
editor?.destroy();
editor = null;
document.getElementById('editor-modal').style.display = 'none';
}
// Alternative: Use Imigi's built-in overlay mode
const overlayEditor = await Imigi.init({
selector: '#editor',
image: 'photo.jpg',
ui: { mode: 'overlay' },
});
// Open on button click
document.getElementById('edit-btn').addEventListener('click', () => {
overlayEditor.open();
});
For the modal pattern, consider using Imigi's built-in overlay mode instead of managing a custom modal. It handles the backdrop, close button, and keyboard shortcuts automatically. See Overlay Mode.
Multiple Editors
You can run multiple Imigi instances on the same page. Each instance is independent and requires its own container.
const editors = new Map();
async function createEditor(containerId, imageUrl) {
// Destroy existing editor in this container
if (editors.has(containerId)) {
editors.get(containerId).destroy();
}
const editor = await Imigi.init({
selector: `#${containerId}`,
image: imageUrl,
onSave(data, filename) {
console.log(`Editor ${containerId} saved: ${filename}`);
},
});
editors.set(containerId, editor);
return editor;
}
// Create multiple editors
await createEditor('editor-1', 'photo1.jpg');
await createEditor('editor-2', 'photo2.jpg');
// Clean up all editors
function destroyAll() {
editors.forEach((editor) => editor.destroy());
editors.clear();
}
Each editor instance creates its own Fabric.js canvas. Running more than two or three editors simultaneously may impact performance on lower-end devices. Consider lazy initialization -- only create editors when they become visible.
File Upload Integration
A common workflow is to let users select an image, edit it with Imigi, and upload the result to a server.
const fileInput = document.getElementById('file-input');
let editor = null;
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const objectUrl = URL.createObjectURL(file);
// Destroy previous editor if exists
editor?.destroy();
editor = await Imigi.init({
selector: '#editor',
image: objectUrl,
async onSave(data, filename) {
// Convert data URL to Blob
const blob = await fetch(data).then((r) => r.blob());
// Build FormData
const formData = new FormData();
formData.append('image', blob, filename);
// Upload to server
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
if (response.ok) {
alert('Image uploaded successfully!');
}
},
});
// Clean up the object URL when done
URL.revokeObjectURL(objectUrl);
});
Form Integration
To include the edited image in a standard HTML form submission, store the image data in a hidden input field:
<!-- HTML -->
<form id="image-form" action="/api/submit" method="POST">
<input type="hidden" name="image_data" id="image-data">
<input type="hidden" name="image_filename" id="image-filename">
<div id="editor" style="width:100%;height:500px;"></div>
<button type="submit">Submit</button>
</form>
<!-- JavaScript -->
<script>
Imigi.init({
selector: '#editor',
image: 'photo.jpg',
onSave(data, filename) {
// Store the edited image data in hidden fields
document.getElementById('image-data').value = data;
document.getElementById('image-filename').value = filename;
},
});
// Validate before submitting
document.getElementById('image-form').addEventListener('submit', (e) => {
const imageData = document.getElementById('image-data').value;
if (!imageData) {
e.preventDefault();
alert('Please save the edited image before submitting.');
}
});
</script>
Data URLs for large images can be very long. For production use, consider uploading the image separately via FormData and storing only a reference (URL or ID) in the form.
Framework Comparison
This table summarizes the key integration points across frameworks:
| Framework | Initialize In | Container Ref | Cleanup In | SSR Handling |
|---|---|---|---|---|
| Vanilla JS | DOMContentLoaded / inline | querySelector |
Manual | N/A |
| React | useEffect |
useRef |
Effect cleanup | next/dynamic |
| Vue 3 | onMounted |
Template ref | onUnmounted |
<ClientOnly> |
| Angular | ngAfterViewInit |
@ViewChild |
ngOnDestroy |
N/A |
| Svelte | onMount |
bind:this |
onDestroy |
onMount (client-only) |