@files-ui/crop
v1.1.0
Published
Framework-agnostic image cropping for Files UI
Maintainers
Readme
@files-ui/crop
Framework-agnostic image cropping for Files UI. Pure JavaScript core with optional React wrapper.
Installation
npm install @files-ui/crop
# or
yarn add @files-ui/cropFeatures
- ✅ Framework Agnostic — Pure JavaScript core works everywhere (Vanilla JS, React, Vue, Angular, Svelte)
- ✅ Zero Dependencies — No external libraries, just Canvas API
- ✅ Touch-Friendly — Mouse drag, touch drag, pinch-zoom, wheel zoom
- ✅ Aspect Ratios — Fixed ratios (1:1, 4:3, 16:9) or free-form
- ✅ Dark Mode — Built-in theming support
- ✅ TypeScript — Full type safety
- ✅ Tiny Bundle — Core: ~5KB, React wrapper: ~2KB (total ~7KB gzipped)
Quick Start
React (Recommended)
import { FileMosaic } from "@files-ui/react";
import { CropDialog, useCropDialog } from "@files-ui/crop/react";
function MyComponent() {
const { cropFile, openCrop, closeCrop, handleCropComplete } = useCropDialog();
return (
<>
<FileMosaic
{...file}
onEdit={openCrop}
/>
{cropFile && (
<CropDialog
file={cropFile}
onComplete={handleCropComplete}
onCancel={closeCrop}
aspectRatio={1} // 1:1 for square crop
/>
)}
</>
);
}
```Package Exports
### `@files-ui/crop` (Main)
Exports both core and React. Use this if you're using React:
```tsx
import { CropDialog, cropImage } from "@files-ui/crop";@files-ui/crop/core (Framework-Agnostic)
Pure JavaScript utilities - no React dependency:
import { CropEngine, cropImage, cropExtFile } from "@files-ui/crop/core";@files-ui/crop/react (React Only)
React components and hooks:
import { CropDialog, useCropDialog } from "@files-ui/crop/react";Core API (Framework-Agnostic)
CropEngine
Interactive vanilla JS crop engine with visual interface.
const engine = new CropEngine(container, options, callbacks);
await engine.loadImage(imageUrl);
engine.setZoom(1.5);
const cropArea = engine.getCropArea();
engine.destroy();Constructor:
container: HTMLElement— DOM element to render intooptions?: CropEngineOptions— Configurationcallbacks?: CropEngineCallbacks— Event callbacks
Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| aspectRatio | number \| null | null | Fixed aspect ratio (e.g., 1 for square) |
| initialZoom | number | 1 | Starting zoom level |
| minZoom | number | 1 | Minimum zoom |
| maxZoom | number | 3 | Maximum zoom |
Methods:
loadImage(url: string): Promise<void>— Load imagesetZoom(level: number): void— Set zoom levelgetZoom(): number— Get current zoom
Usage Examples
Vanilla JavaScript (No Framework)
<!DOCTYPE html>
<html>
<body>
<div id="crop-container" style="width: 600px; height: 400px;"></div>
<input type="range" id="zoom" min="1" max="3" step="0.1" value="1" />
<button id="save">Save Crop</button>
<script type="module">
import { CropEngine, cropExtFile } from "@files-ui/crop/core";
const engine = new CropEngine(
document.getElementById("crop-container"),
{ aspectRatio: 16/9 },
{ onZoomChange: (z) => console.log("Zoom:", z) }
);
// Load image
await engine.loadImage("photo.jpg");
// Zoom control
document.getElementById("zoom").oninput = (e) => {
engine.setZoom(Number(e.target.value));
};
// Save
document.getElementById("save").onclick = async () => {
const cropArea = engine.getCropArea();
const file = { imageUrl: "photo.jpg", name: "photo.jpg" };
const cropped = await cropExtFile(file, cropArea);
console.log("Cropped file:", cropped);
};
</script>
</body>
</html>React with Files UI
import { Dropzone, FileMosaic } from "@files-ui/react";
import { CropDialog, useCropDialog } from "@files-ui/crop/react";
function ImageUploader() {
const [files, setFiles] = useState([]);
const { cropFile, openCrop, closeCrop, handleCropComplete } = useCropDialog({
onComplete: (cropped) => {
setFiles(prev => prev.map(f =>
f.id === cropped.id ? cropped : f
));
}
});
return (
<>
<Dropzone value={files} onChange={setFiles}>
{files.map(file => (
<FileMosaic key={file.id} {...file} onEdit={openCrop} />
))}
</Dropzone>
{cropFile && (
<CropDialog
file={cropFile}
onComplete={handleCropComplete}
onCancel={closeCrop}
aspectRatio={1}
darkMode
/>
)}
</>
);
}Custom Aspect Ratios
<CropDialog
file={file}
aspectRatio={16 / 9} // Widescreen
onComplete={handleCrop}
onCancel={closeCrop}
/>Low-Level Cropping (No UI)
import { cropImage } from "@files-ui/crop/core";
// Crop specific area
const blob = await cropImage(
imageFile,
{ x: 100, y: 100, width: 300, height: 300 },
{ quality: 0.95, format: "webp" }
);
// Use the blob
const url = URL.createObjectURL(blob);nst blob = await cropImage(imageSource, cropArea, options);Parameters:
| Param | Type | Description |
|-------|------|-------------|
| imageSource | string \| File \| Blob | Image to crop |
| cropArea | CropArea | Crop coordinates |
| options | CropOptions | Quality/format settings |
Returns: Promise<Blob>
cropExtFile()
Crop an ExtFile and return new ExtFile:
import { cropExtFile } from "@files-ui/crop/core";
const croppedFile = await cropExtFile(file, cropArea, options);React API
Vanilla JavaScript (Framework-Agnostic)
import { CropEngine, cropExtFile } from "@files-ui/crop/core";
// Create crop engine
const container = document.getElementById("crop-container");
const engine = new CropEngine(container, { aspectRatio: 1 });
Architecture
@files-ui/crop ├── core/ # Framework-agnostic (5KB) │ ├── cropImage.ts # Pure Canvas cropping │ ├── CropEngine.ts # Interactive UI engine │ ├── extFileCrop.ts # ExtFile helpers │ └── types.ts # TypeScript definitions └── react/ # React wrapper (2KB) ├── CropDialog.tsx # React component ├── useCropDialog.ts # React hook └── CropDialog.css # Styles
**Benefits:**
- Use `core` in any framework (Vue, Angular, Svelte, vanilla JS)
- Use `react` for seamless React integration
- No external dependencies - just Canvas API
- Tiny bundle size (~7KB total)
## Browser Support
Works in all modern browsers with Canvas API:
- Chrome 60+
- Firefox 55+
- Safari 11+
- Edge 79+
##
// Load image
await engine.loadImage(file.imageUrl);
// User interacts (drag, zoom, etc.)
// Get crop area and create cropped file
document.getElementById("save-btn").onclick = async () => {
const cropArea = engine.getCropArea();
const croppedFile = await cropExtFile(file, cropArea, {
quality: 0.9,
format: "jpeg"
});
console.log("Cropped:", croppedFile);
};API Reference
<CropDialog>
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| file | ExtFile | required | The file to crop |
| onComplete | (croppedFile: ExtFile) => void | required | Called with cropped file |
| onCancel | () => void | required | Called when user cancels |
| aspectRatio | number \| null | null | Aspect ratio (e.g., 1 for square, 16/9 for widescreen) |
| darkMode | boolean | false | Enable dark mode styling |
| quality | number | 0.92 | Output quality (0-1) |
| format | "jpeg" \| "png" \| "webp" | "jpeg" | Output format |
useCropDialog()
Hook for managing crop dialog state:
const {
cropFile, // Current file being cropped
openCrop, // (file: ExtFile) => void
closeCrop, // () => void
handleCropComplete // (croppedFile: ExtFile) => void
} = useCropDialog({
onComplete: (file) => {
// Replace original with cropped
updateFile(file);
}
});Advanced Usage
Custom Aspect Ratios
<CropDialog
file={file}
aspectRatio={16 / 9} // Widescreen
onComplete={handleCrop}
onCancel={closeCrop}
/>With Multiple Files
function MultiFileCrop() {
const [files, setFiles] = useState([]);
const { cropFile, openCrop, closeCrop } = useCropDialog({
onComplete: (croppedFile) => {
setFiles(prev => prev.map(f =>
f.id === croppedFile.id ? croppedFile : f
));
}
});
return (
<>
<Dropzone value={files} onChange={setFiles}>
{files.map(file => (
<FileMosaic key={file.id} {...file} onEdit={openCrop} />
))}
</Dropzone>
{cropFile && (
<CropDialog
file={cropFile}
onComplete={() => closeCrop()}
onCancel={closeCrop}
/>
)}
</>
);
}License
MIT © JinSSJ3
