If you are looking to crop and upload images like in Instagram, please visit https://github.com/avidofood/vue-cropgram 😜
Prerequisites: Vue 3.2 or newer for version 2.x of this package. For Vue 2, use version 1.x.
# For Vue 3.x.x
npm install vue-instagram-cropper
# For Vue 2.x.x
npm install vue-instagram-cropper@1x<script setup>
import InstagramCropper from 'vue-instagram-cropper';
</script>Or register it globally:
import { createApp } from 'vue';
import InstagramCropper from 'vue-instagram-cropper';
const app = createApp(App);
app.component('InstagramCropper', InstagramCropper);import { createApp } from 'vue';
import { Plugin } from 'vue-instagram-cropper';
const app = createApp(App);
app.use(Plugin);The plugin registers the component as InstagramCropper. You can use it as <InstagramCropper> or <instagram-cropper>.
The component renders on the server without errors. The canvas draws the image in the browser after the hydration. Import the component where you need it, or register it in a plugin:
// plugins/instagram-cropper.js
import { Plugin } from 'vue-instagram-cropper';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(Plugin);
});Since version 2.0.0 the package contains type declarations for the props, the metadata, the events, the methods and the plugin.
import type { InstagramCropperEmits } from 'vue-instagram-cropper';
// Right after remove(), the metadata can have no image
const onUpdate: InstagramCropperEmits['update'] = (metadata) => {
if (metadata.img) console.log(metadata.imgData);
}; <instagram-cropper
:src="cropper"
></instagram-cropper>
with
cropper: 'https://i.picsum.photos/id/468/200/300.jpg',The component takes the size of its container. Give it a width and a height, for example with a class. When the container changes its size, the canvas follows. This also works for a container that is hidden at first, for example with v-show. Without ResizeObserver, for example in jsdom, the size stays as it was at the mount.
<instagram-cropper
ref="cropper"
class="w-100 h-100"
:src="cropper"
:quality="4"
:placeholder-font-size="14"
placeholder-color="#000000"
placeholder="Choose or Drag'n'Drop an image"
>
<spinner v-if="loading"/>
</instagram-cropper>https://avidofood.github.io/vue-instagram-cropper
This package is made to imitate Instagram's cropper.
src(required:false, type: String or Object)
You can set the source of the placeholder image here. You can also use objects to set images. Test it by saving the meta-object by calling the method getMetadata().
quality(default:2)
Multiplies your image output. If your canvas is 600px wide and the quality is set to 2, the output width will be 1200.
-
canvasColor(default:#F7F7F7) -
placeholder(default:Choose an image) -
placeholderColor(default:#67ACFD) -
placeholderFontSize(default:0)
With 0, the component calculates the font size.
fileSizeLimit(default:0)
The largest file size in bytes. 0 means no limit. A larger file emits file-size-exceed.
crossOrigin(default:anonymous)
The CORS mode for an image from another origin. The image server must allow the export with the header Access-Control-Allow-Origin. Otherwise the browser does not export the cropped image. If the image server needs the cookies of the user, set use-credentials. Then the server must also send Access-Control-Allow-Credentials: true and your origin instead of *.
forceCacheBreak(default:false)
If you still have CORS issues, set it to true. The browser then does not cache the images. Data URLs and blob URLs stay as they are.
preventWhiteSpace(default:false)
Keeps the canvas filled with the image while the user moves or zooms it.
showGrid(default:true)
Shows the rule-of-thirds grid while the user moves or zooms the image, as in Instagram.
labels(default: English texts)
Texts for screen readers: canvas, remove and fullscreen. See Keyboard and screen readers.
zoomOnWheel(default:true)
With false, the mouse wheel and the trackpad do not zoom the image, and the page scrolls over the cropper. Pinch to zoom still works.
Photos from a phone often contain an EXIF orientation. Browsers turn these images correctly by themselves since 2020 (Chrome 81, Firefox 77, Safari 13.1). Version 2.0 draws the image as the browser shows it.
The canvas can get the focus with the Tab key. While it has the focus:
- Without an image, Enter or Space opens the file chooser.
- The arrow keys move the image by 10 pixels. With Shift, they move it by 50 pixels.
- Plus and minus zoom the image.
The canvas, the remove button and the fullscreen button have English texts for screen readers. Replace them with the prop labels, for example:
<instagram-cropper
:src="cropper"
:labels="{ remove: 'Bild entfernen', fullscreen: 'Bild einpassen oder füllen' }"
></instagram-cropper>labels.canvas describes the canvas and its keys.
init: The component is ready. The event carries the component.initial-image-loaded: The image fromsrcloaded for the first time.file-choose: The user chose a file. The event carries the file.file-size-exceed: The file is larger thanfileSizeLimit.file-type-mismatch: The file is not an image.file-loaded: The component read the new file.new-image: The component read a new valid image.new-image-drawn: The component drew a new image on the canvas for the first time.image-error: The image failed to load (onerrorlistener).image-remove: The user orremove()removed the image.image-remove-onload: The component removed the old image for a new file or a newsrc.move: The user ormove()moved the image.zoom: The user orzoom()changed the size of the image.draw: The component drew the view again. The event carries the canvas context.loading-start: The image starts to load.loading-end: The image finished loading.update: The component drew a new view. The event carries the metadata, seegetMetadata(). Right afterremove(), the metadata can have no image (imgisnull).input: The component removed the image. The value isnull.
The component also emits native events of the canvas again. These are click, dblclick, mousedown, mouseup, mousemove and wheel. These are touchstart, touchend, touchcancel and touchmove. These are pointercancel, pointermove and pointerleave. For the drag and drop of a file, it emits dragenter, dragleave, dragover and drop of the container.
You need to set ref="cropper" to the HTML tag <instagram-cropper>. After that you can call all methods like this this.$refs.cropper.hasImage().
With <script setup>, use a template ref:
<script setup>
import { ref } from 'vue';
import InstagramCropper from 'vue-instagram-cropper';
const cropper = ref(null);
const save = async () => {
const blob = await cropper.value.promisedBlob('image/jpeg', 0.8);
};
</script>
<template>
<InstagramCropper ref="cropper" src="/images/photo.jpg" />
</template>getCanvas(): returns the canvas objectgetContext(): returns the canvas context objectgetChosenFile(): returns File objectchooseFile(): Opens the file chooser window to choose an image.refresh(): Starts the component again, for example to load the initial image again.hasImage(): Return boolean value indicating whether currently there is a image.remove(): Removes the image and shows the placeholder.move({ x: number, y: number }): Moves the image byxandycanvas pixels.zoom( zoomIn: boolean, acceleration: number ): Zooms in or out by one step.zoomIndefaults totrue,accelerationto 1.generateDataUrl( type: string, compressionRate: number, options: object ):- Returns a data-URL containing a representation of the image in the format specified by the type parameter (defaults to png).
compressionRatedefaults to 1, you can pass a number between 0 and 1 to get a compressed output image.optionssets the output size, see Output size.- If there is no image, it returns an empty string.
generateBlob( callback: function, mimeType: string, compressionRate: number, options: object ):- Creates a Blob object representing the image contained in the canvas.
- If there is no image, the first argument of callback function is null.
promisedBlob( mimeType: string, compressionRate: number, options: object ):- Returns a Promise around
generateBlob(). With it, you can use async/await instead of a callback. - If there is no image, the promise resolves with
null.
- Returns a Promise around
getMetadata(): Returns an object with the image, its position and its scale. Pass it tosrcto show the image with the same crop again, for example in a list of images.saving(img, imgData, outputWidth, outputHeight): Creates the output for other metadata, for example for an image in a list. It returns an object withgenerateDataUrl(),generateBlob()andpromisedBlob(). Pass theoutputWidthandoutputHeightof the component.
const blob = await this.$refs.cropper.promisedBlob()Without options, the output has the size of the visible image in canvas pixels: the container size times quality. The last argument of generateDataUrl(), generateBlob(), promisedBlob() and of the object from saving() sets another size:
widthorheight: that side gets this size. The other side keeps the aspect ratio.widthandheight: the output fits into this box.maxWidthandmaxHeight: a larger output gets smaller. These options never make the output larger.
// Always 1080 pixels wide, for example for Instagram
const blob = await this.$refs.cropper.promisedBlob('image/jpeg', 0.9, { width: 1080 });
// At most 2048 pixels on each side
const url = this.$refs.cropper.generateDataUrl('image/png', 1, { maxWidth: 2048, maxHeight: 2048 });Give the container the aspect ratio and set prevent-white-space. Then the image always fills the container, and the output has the same aspect ratio. Together with a fixed output width, every image gets the same size:
<div style="width: 100%; max-width: 400px; aspect-ratio: 4 / 5;">
<instagram-cropper
ref="cropper"
:src="cropper"
prevent-white-space
style="width: 100%; height: 100%;"
></instagram-cropper>
</div>// 1080 x 1350 pixels, the portrait size of Instagram
const blob = await this.$refs.cropper.promisedBlob('image/jpeg', 0.9, { width: 1080 });A size larger than the original image makes the output blurry. An output side over 32767 pixels throws a RangeError, because browsers cannot draw such a canvas. Some browsers allow less, for example iOS Safari about 16 megapixels in total. Use maxWidth and maxHeight to stay below that.
addClipPlugin(func): Add clip plugin to clip the image. Example:
// Add a clip plugin to make a circle clip on image
onInit(vm) {
this.$refs.cropper.addClipPlugin(function (ctx, x, y, w, h) {
/*
* ctx: canvas context
* x: start point (top-left corner) x coordination
* y: start point (top-left corner) y coordination
* w: croppa width
* h: croppa height
*/
ctx.beginPath();
ctx.arc(x + w / 2, y + h / 2, w / 2, 0, 2 * Math.PI, true);
ctx.closePath();
})
},Note: In the plugin function, always start with
ctx.beginPath()and end withctx.closePath().
Note: Clip plugins only work with
prevent-white-spaceset totrue.
To remove all clip plugins, set this.$refs.cropper.clipPlugins = null.
Version 2.0 is for Vue 3. The props, the events and the methods have the same names as in 1.x. These things changed:
-
Vue 3.2 or newer is required. For Vue 2, stay on version 1.x:
npm install vue-instagram-cropper@1x. -
Register the component with
app.component()orapp.use(Plugin)instead ofVue.component()orVue.use(). The plugin registers the nameInstagramCropper. The tag<instagram-cropper>still works. -
Remove
.nativefrom listeners on the component. Vue 3 has no.nativemodifier. A listener such as@clickgets the click on the canvas from the component, as in 1.x without.native. -
The component no longer turns a chosen photo by its EXIF orientation. The browser does that already. In 1.x, photos from the iOS photo editor showed up turned twice (#17).
-
The component no longer listens to and emits the legacy events
DOMMouseScrollandmousewheel. Usewheel. -
If there is no image,
promisedBlob()resolves withnull. In 1.x, the promise was rejected. -
With
preventWhiteSpace, zooming out at the smallest size emits nozoomevent and does not move the image. -
The package contains only the build in
dist/. Import fromvue-instagram-cropper. Imports such asvue-instagram-cropper/src/...orvue-instagram-cropper/dist/index.common.jsno longer work. The UMD build sets the global variableVueInstagramCropperinstead ofindex. -
The package no longer adds polyfills for
requestAnimationFrameandcanvas.toBlob(). Every browser that Vue 3 supports has both. The 1.x polyfill also replacedArray.isArrayon the whole page. -
The package has no runtime dependencies.
canvas-exif-orientationis gone. -
Some fixes change what you can observe:
- After a drag, the component no longer emits
mouseupand the other end events for clicks elsewhere on the page. remove()andsrcset tonullstop an image that still loads and emitloading-end.- A slower image or file no longer replaces a newer one.
- Several croppers on one page no longer share their image.
- With
preventWhiteSpace, metadata whose crop does not fill the canvas shows the image filled and centered. - Metadata of another image with the same size and crop draws the new image.
forceCacheBreakworks with relative URLs and keeps data URLs and blob URLs as they are.- The container gets the class
cropper--dropzonewhile a file is over it.
The CHANGELOG lists all fixes.
- After a drag, the component no longer emits
You need Node.js 22.22.2 or newer in version 22, or 24.15.0 or newer in version 24 (see .nvmrc). jsdom 30 needs these versions for the tests.
npm install
npm test # unit tests and type checks
npm run lint
npm run build # builds dist/ and the demonpm pack and npm publish build dist/ first.
- Set the new version in
package.jsonand add it toCHANGELOG.md. - Merge the change into
master. - Push a tag with the version number, for example
git tag 2.0.1 && git push origin 2.0.1.
The Release workflow then runs the lint and the tests, and publishes the package to npm. It uses npm trusted publishing, so it needs no npm token and no 2FA prompt. The tag must match the version in package.json and must be on master. Run the workflow by hand to check the setup. That run publishes nothing.
On npmjs.com, the trusted publisher of the package points to this repository, the workflow release.yml and the environment npm-publish. Under "Allowed actions", it must allow npm publish. A new trusted publisher expires after 2 days without a publish. Create it right before a release.
I have only limited time to develop this package further. Your help to improve it step by step means a lot to me. Here is a small list of what is still missing:
- The maximum zoom limit is not the same as in Instagram
- We need the prop
forceAspect. With it, you can "clip" the image to a specific aspect ratio. vue-cropgram needs it for multiple images.
If you discover any security related issues, please do not email me. I'm afraid 😱. avidofood@protonmail.com
Now comes the best part! 😍 This package is based on
- https://github.com/zhanziyang/vue-croppa (but simplified)
Oh come on. You read everything?? If you liked it so far, hit the ⭐️ button to give me a 🤩 face.
See CHANGELOG.md.
