@get-set/gs-cropper
v1.0.2
Published
Get-Set Cropper
Maintainers
Readme
GSCropper
A dependency-free, feature-rich image cropper available in two flavours from one codebase:
- Native / vanilla JS — a
window.GSCropper(selector | element, params)factory plus awindow.GSCropperConfigueinstance registry, a jQuery plugin ($.fn.GSCropper) and anHTMLElement.prototypeadapter. - React — a
<GSCropper src="…" />component with a fully typed imperative ref handle (GSCropperHandle).
Both share the exact same engine (actions/, constants/, helpers/, types/), so behaviour is identical across the two. Drag/zoom/rotate/flip, resizable crop box, rule-of-thirds guides, circular avatar mode, wheel + pinch zoom, keyboard nudging, and canvas/blob/dataURL export are all built in.
Features
- Enhance an
<img>or a container — the native factory accepts a CSS selector or a real DOM element; if you point it at a wrapper it finds the<img>inside. - Aspect ratio —
free, a raw number (w / h), or a preset string (1:1,4:3,3:4,16:9,9:16,3:2,2:3,5:4,4:5,21:9,2:1,1:2), changeable at runtime viasetAspectRatio. - Zoom — mouse wheel + pinch (touch), with
minZoom/maxZoomclamping and a tunablewheelZoomRatio. - Rotate + flip — rotate by any step (default 90°) and flip horizontally / vertically.
- Draggable / resizable crop box — 8 resize handles, drag the whole box, or pan the image behind it (
movable/pannable). - View / boundary modes (
viewMode0–3, Cropper.js parity) keep the crop box inside the canvas. - Guides / grid overlay —
thirds,golden,grid,center, or off, plus a center indicator cross. - Circular crop (avatar) — round crop box and a round export mask.
- Min / max crop-box size constraints.
- Backdrop layer catalog —
checkerboard,grid,solid,blur,glass,gradient,aurorabehind transparent images. - Overlay dimming styles —
dim,darken,blur,none. - Responsive — rescales the image + crop proportionally on container resize (ResizeObserver).
- Initial crop —
autoCropfraction or an explicitinitialCroprectangle in natural pixels. - Theming —
light/dark/auto(follows OS) plus a customaccentColortoken, RTL, and adisabledstate. - A11y — focusable crop box with arrow-key nudging (
+/-zoom,[/]rotate), ARIA roles/labels, andprefers-reduced-motionsupport. - Smooth animations — micro-interactions on drag/zoom/rotate and crop-box transitions (disabled during active drag and under reduced motion).
- Export —
getCroppedCanvas,getDataURL,getBlob(with fill color, output size, MIME/quality, and rounded masking). - Callbacks —
onReady,onCrop,onCropStart,onCropEnd,onZoom,onRotate,onFlip. - React-only
gsxprop for per-instance scoped styles.
Installation
npm i @get-set/gs-cropperCompatibility
| Target | Requirement |
|---|---|
| React (<GSCropper>) | React 16.8+ (Hooks), and 17 / 18 / 19. React is an optional peer dependency. |
| Native / vanilla (window.GSCropper) | No framework. Any modern evergreen browser. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import server-side. The cropper is browser-only, so render it inside a Client Component ('use client'). |
The peer range is
^16.8.0 || ^17.0 || ^18.0 || ^19.0, withreact/react-dommarked optional so the native build has zero peer dependencies.
Package entry points
| Field | Value |
|---|---|
| name | @get-set/gs-cropper |
| main | dist/components/GSCropper.js |
| module | dist/components/GSCropper.js |
| types | dist/components/GSCropper.d.ts |
| exports['.'].import | ./dist/components/GSCropper.js (React/ESM) |
| exports['.'].require | ./dist-js/bundle.js (native window bundle) |
| files | dist, dist-js, styles, example.html |
Project layout
GSCropper.ts # native entry (webpack -> dist-js/bundle.js, window global)
components/GSCropper.tsx # React component (tsc -> dist/, npm entry)
actions/ # shared engine (init/render/events/api/refresh/destroy)
constants/ # aspectRatios, viewModes, defaultParams
helpers/ # geometry, interaction, layout, markup, uihelpers
types/ # Params / Ref / Window augmentation
components/styles/ # SCSS + compiled CSS + CSS-as-TS (runtime injection for React)
styles/ # SCSS + compiled CSS (for <link> use by the native build)Build
npm install
npm run build # builds both targets
npm run build:js # native bundle -> dist-js/bundle.js
npm run build:react # React + types -> dist/Tests
Unit tests use Vitest + jsdom:
npm test # run once
npm run test:watchCoverage spans the pure logic (aspect resolution, geometry, interaction math, param merge, layout/markup helpers), native DOM behaviour (registry, structure, the full imperative API, drag/resize/wheel/keyboard/toolbar wiring, destroy/refresh), the jQuery + prototype adapters (which prove the factory accepts an element), and the React component (structure, imperative handle, gsx, prop-stripping, registry parity). jsdom has no layout engine, so pixel-exact positioning is validated in-browser; the units mock element boxes + canvas.
Usage — Native JS
Point the factory at an <img> (or a container holding one):
<link rel="stylesheet" href="styles/GSCropper.css" />
<img id="avatar" src="portrait.jpg" alt="Portrait" />
<script src="dist-js/bundle.js"></script>
<script>
const cropper = window.GSCropper('#avatar', {
aspectRatio: '1:1',
circular: true,
zoomable: true,
guides: 'thirds',
toolbar: true,
onCrop: (data) => console.log(data)
});
</script>GSCropper is exposed as a global factory. Call it with a CSS selector string or a real element — this is essential, because the jQuery / prototype adapters pass an element:
window.GSCropper('#avatar', { aspectRatio: '1:1' });
window.GSCropper(document.querySelector('#avatar'), { aspectRatio: '1:1' });jQuery plugin
// when jQuery is present on the page
$('#avatar').GSCropper({ aspectRatio: '16:9', guides: 'golden' });HTMLElement.prototype
document.querySelector('#avatar').GSCropper({ circular: true });Both adapters create the cropper exactly like the factory and register it in
window.GSCropperConfigue.
The instance registry — window.GSCropperConfigue
Every cropper registers itself under its reference key. Look up a live instance and drive it:
window.GSCropper('#avatar', { reference: 'main', aspectRatio: '1:1' });
const inst = window.GSCropperConfigue.instance('main');
inst.zoom(0.2); // zoom in
inst.rotate(90); // rotate 90°
inst.flipX(); // mirror horizontally
inst.setAspectRatio('16:9'); // change ratio at runtime
const url = inst.getDataURL(); // export as data URL
inst.reset(); // back to the initial crop
inst.destroy(); // tear down + restore the <img>window.GSCropperConfigue shape:
| Member | Type | Description |
|---|---|---|
| references | Array<{ key: string; ref: Ref }> | All registered instances. |
| instance(key) | (key: string) => Ref \| undefined | Look up an instance's runtime Ref by its reference key. |
Native instance methods (Ref)
The object returned by window.GSCropper(...) (and stored in the registry) exposes:
| Method | Signature | Description |
|---|---|---|
| getCroppedCanvas | (options?) => HTMLCanvasElement \| null | Render the crop to a canvas at natural resolution. |
| getData | () => GSCropData | Crop rectangle in natural image pixels (+ rotate/scale). |
| setData | (data: Partial<GSCropData>) => void | Set the crop rect / rotation / flip. |
| getBlob | (options?) => Promise<Blob \| null> | Export the crop as a Blob. |
| getDataURL | (options?) => string \| null | Export the crop as a data URL. |
| zoom | (delta: number) => void | Zoom by a relative amount (+ in, − out). |
| zoomTo | (scale: number) => void | Zoom to an absolute scale. |
| rotate | (deg: number) => void | Rotate by a relative angle. |
| rotateTo | (deg: number) => void | Rotate to an absolute angle. |
| flipX / flipY | () => void | Flip horizontally / vertically. |
| flip | (axis: 'horizontal' \| 'vertical') => void | Flip on an axis. |
| move | (x: number, y: number) => void | Nudge the crop box by canvas px. |
| setAspectRatio | (aspect: number \| string \| 'free') => void | Change the crop-box ratio at runtime. |
| reset | () => void | Restore the initial image + auto crop. |
| clear | () => void | Empty the crop box. |
| enable / disable | () => void | Toggle interaction. |
| refresh | () => void | Rebuild from (updated) params. |
| destroy | () => void | Tear down + restore the original element. |
Usage — React
'use client';
import { useRef } from 'react';
import GSCropper, { GSCropperHandle } from '@get-set/gs-cropper';
export default function AvatarEditor() {
const ref = useRef<GSCropperHandle>(null);
const save = async () => {
const blob = await ref.current!.getBlob({ width: 256, height: 256 });
// upload blob…
};
return (
<>
<GSCropper
ref={ref}
src="/portrait.jpg"
alt="Portrait"
aspectRatio="1:1"
circular
guides="thirds"
toolbar
theme="auto"
accentColor="#2563eb"
onCrop={(data) => console.log(data)}
onReady={() => console.log('ready')}
/>
<button onClick={() => ref.current!.rotate(90)}>Rotate</button>
<button onClick={() => ref.current!.flip('horizontal')}>Flip</button>
<button onClick={save}>Save</button>
</>
);
}The imperative handle — GSCropperHandle
useImperativeHandle exposes full parity with the native Ref:
| Method | Signature |
|---|---|
| getCroppedCanvas | (options?: GSCropperOutputOptions) => HTMLCanvasElement \| null |
| getData | () => GSCropData |
| setData | (data: Partial<GSCropData>) => void |
| getBlob | (options?) => Promise<Blob \| null> |
| getDataURL | (options?) => string \| null |
| zoom | (delta: number) => void |
| zoomTo | (scale: number) => void |
| rotate | (deg: number) => void |
| rotateTo | (deg: number) => void |
| flipX | () => void |
| flipY | () => void |
| flip | (axis: 'horizontal' \| 'vertical') => void |
| move | (x: number, y: number) => void |
| reset | () => void |
| clear | () => void |
| setAspectRatio | (aspect: number \| string \| 'free') => void |
| enable | () => void |
| disable | () => void |
| destroy | () => void |
React components also register in window.GSCropperConfigue, so window.GSCropperConfigue.instance('your-reference') returns the same imperative API from anywhere.
The gsx prop (React only)
Scoped, per-instance styles injected into <head> and removed on unmount. Keys are nested selectors relative to this instance ([data-key='…']):
<GSCropper
src="/pic.jpg"
gsx={{
'.gs-cropper-crop-box': { outlineColor: '#f43f5e' },
'.gs-cropper-handle': { background: '#f43f5e' }
}}
/>Options
Every option (native params and React props are identical):
| Option | Type | Default | Description |
|---|---|---|---|
| reference | string | auto GUID | Registry key. |
| src | string | host <img src> | Image source URL. |
| alt | string | '' | Image alt text. |
| crossOrigin | '' \| 'anonymous' \| 'use-credentials' | — | For taint-free canvas export of remote images. |
| aspectRatio | 'free' \| number \| preset | 'free' | Crop-box ratio (1:1, 16:9, … or w/h). |
| initialAspectRatio | 'free' \| number \| preset | — | Ratio applied only to the initial auto crop. |
| circular | boolean | false | Round (avatar) crop box + round export mask. |
| viewMode | 0 \| 1 \| 2 \| 3 | 1 | Boundary mode (Cropper.js parity). |
| background | boolean | true | Show the checkerboard/grid behind transparency. |
| responsive | boolean | true | Rescale on container resize. |
| restore | boolean | true | Restore the crop on resize. |
| containerAspectRatio | number | — | Fixed canvas aspect ratio (w/h). |
| autoCrop | number \| false | 0.8 | Initial crop area as a fraction of the canvas; false = none. |
| initialCrop | Partial<GSCropData> | — | Explicit initial crop in natural pixels (overrides autoCrop). |
| zoomable | boolean | true | Enable zoom (wheel + pinch). |
| zoomOnWheel | boolean | true | Enable wheel zoom. |
| zoomOnPinch | boolean | true | Enable pinch zoom. |
| wheelZoomRatio | number | 0.1 | Wheel/keyboard zoom step. |
| minZoom | number | 0.1 | Minimum zoom (× fit). |
| maxZoom | number | 10 | Maximum zoom (× fit). |
| rotatable | boolean | true | Enable rotation. |
| flippable | boolean | true | Enable flipping. |
| rotateStep | number | 90 | Rotate step (deg) for keyboard/toolbar. |
| draggable | boolean | true | Drag the whole crop box. |
| movable | boolean | true | Allow moving the image. |
| pannable | boolean | true | Pan the image with a drag on the canvas. |
| resizable | boolean | true | Resize the crop box via handles. |
| handles | boolean | true | Render the resize handles. |
| nudgeStep | number | 1 | Arrow-key nudge amount (px). |
| minCropWidth | number | 20 | Minimum crop-box width. |
| minCropHeight | number | 20 | Minimum crop-box height. |
| maxCropWidth | number | — | Maximum crop-box width. |
| maxCropHeight | number | — | Maximum crop-box height. |
| overlay | 'dim' \| 'blur' \| 'darken' \| 'none' | 'dim' | Outside-the-box dimming style. |
| guides | boolean \| 'thirds' \| 'golden' \| 'grid' \| 'center' | true | Grid / guide lines. |
| center | boolean | true | Center indicator cross. |
| backdropType | 'none' \| 'solid' \| 'blur' \| 'glass' \| 'gradient' \| 'checkerboard' \| 'grid' \| 'aurora' | 'checkerboard' | Canvas backdrop layer. |
| toolbar | boolean | false | Built-in zoom/rotate/flip/reset toolbar. |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme. |
| accentColor | string | #2563eb | Accent (--gsc-accent) for handles/focus. |
| rtl | boolean | false | Right-to-left layout. |
| disabled | boolean | false | Disable all interaction. |
| reducedMotion | 'auto' \| boolean | 'auto' | Honor prefers-reduced-motion. |
| customClass | GSCropperCustomClass | — | Per-part class overrides. |
| className | string | — | Extra class on the container. |
| output | GSCropperOutputOptions | — | Default export options. |
| gsx | NestedCSS | — | React only — scoped inline styles. |
| onReady | () => void | — | Image loaded + cropper laid out. |
| onCrop | (data: GSCropData) => void | — | Crop rectangle changed. |
| onCropStart | (action: string) => void | — | Drag/resize interaction started. |
| onCropEnd | (action: string) => void | — | Drag/resize interaction ended. |
| onZoom | (scale: number) => void | — | Zoom level changed. |
| onRotate | (angle: number) => void | — | Image rotated. |
| onFlip | (flip: { scaleX; scaleY }) => void | — | Image flipped. |
GSCropData
interface GSCropData {
x: number; // crop left in natural image pixels
y: number; // crop top in natural image pixels
width: number; // crop width in natural pixels
height: number; // crop height in natural pixels
rotate: number; // image rotation (deg)
scaleX: number; // horizontal flip (±1)
scaleY: number; // vertical flip (±1)
}GSCropperOutputOptions
interface GSCropperOutputOptions {
width?: number; // output width (default: crop width)
height?: number; // output height (default: crop height)
fillColor?: string; // paint behind transparent/rotated corners
imageSmoothingQuality?: 'low' | 'medium' | 'high';
imageSmoothingEnabled?: boolean;
mimeType?: string; // toDataURL / toBlob MIME (default image/png)
quality?: number; // 0–1 for lossy MIME
rounded?: boolean; // force a circular mask (defaults to `circular`)
}Variant catalogs
aspectRatio presets
1:1 · 4:3 · 3:4 · 16:9 · 9:16 · 3:2 · 2:3 · 5:4 · 4:5 · 21:9 · 2:1 · 1:2 — or a raw number (1.7778) or 'free'.
viewMode
| Value | Behaviour |
|---|---|
| 0 | No restriction — the crop box may leave the canvas. |
| 1 | Crop box is restricted to the canvas (default). |
| 2 | The image covers and is restricted to the crop box. |
| 3 | The image covers and is restricted to the container. |
guides
thirds (rule of thirds) · golden (golden ratio) · grid (4×4) · center (cross only) · true (= thirds) · false (off).
backdropType
checkerboard (default) · grid · solid · blur · glass · gradient · aurora (animated).
overlay
dim (default) · darken · blur · none.
Events / callbacks
window.GSCropper('#img', {
onReady: () => {},
onCrop: (data) => {}, // { x, y, width, height, rotate, scaleX, scaleY }
onCropStart: (action) => {}, // 'move' | 'pan' | 'e' | 'se' | …
onCropEnd: (action) => {},
onZoom: (scale) => {},
onRotate: (deg) => {},
onFlip: ({ scaleX, scaleY }) => {}
});Examples
Native — square avatar with circular mask, dark theme, toolbar
<img id="ava" src="face.jpg" alt="Avatar" />
<script src="dist-js/bundle.js"></script>
<script>
const c = window.GSCropper('#ava', {
reference: 'ava',
aspectRatio: '1:1',
circular: true,
theme: 'dark',
accentColor: '#22d3ee',
guides: 'center',
toolbar: true,
minCropWidth: 64,
minCropHeight: 64
});
document.querySelector('#save').addEventListener('click', async () => {
const blob = await c.getBlob({ width: 256, height: 256, mimeType: 'image/png' });
// upload blob…
});
</script>Native — free-form crop with golden guides + wheel zoom
window.GSCropper(document.querySelector('#photo'), {
aspectRatio: 'free',
guides: 'golden',
overlay: 'darken',
backdropType: 'grid',
wheelZoomRatio: 0.15,
maxZoom: 6
});React — controlled 16:9 banner cropper
'use client';
import { useRef, useState } from 'react';
import GSCropper, { GSCropperHandle } from '@get-set/gs-cropper';
export default function BannerCropper() {
const ref = useRef<GSCropperHandle>(null);
const [preview, setPreview] = useState<string>('');
return (
<div>
<GSCropper
ref={ref}
src="/banner.jpg"
aspectRatio="16:9"
guides="thirds"
toolbar
onCrop={() => setPreview(ref.current!.getDataURL() ?? '')}
/>
{preview && <img src={preview} alt="Preview" />}
</div>
);
}React — scoped styling with gsx
<GSCropper
src="/pic.jpg"
aspectRatio="4:3"
gsx={{
'.gs-cropper-crop-box': { outline: '2px solid #f43f5e' },
'.gs-cropper-handle': { borderColor: '#f43f5e', background: '#fff' }
}}
/>License
ISC © Get-Set
