Here’s how to implement Cropper.js with a tile-triggered modal upload in WordPress, using Bootstrap for the modal (which works well with Cropper.js controls):
1. Enqueue Required Scripts & Styles
function enqueue_cropper_assets() {
// Bootstrap CSS (for modal)
wp_enqueue_style('bootstrap-css', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css');
// Cropper.js CSS
wp_enqueue_style('cropper-css', 'https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.css');
// Bootstrap JS (for modal)
wp_enqueue_script('bootstrap-js', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js', ['jquery'], null, true);
// Cropper.js
wp_enqueue_script('cropper-js', 'https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js', [], null, true);
// Custom JS for handling Cropper in modal
wp_enqueue_script('custom-cropper', get_stylesheet_directory_uri() . '/js/custom-cropper.js', ['jquery', 'cropper-js'], null, true);
}
add_action('wp_enqueue_scripts', 'enqueue_cropper_assets');
2. Create the Tile + Modal HTML (Shortcode)
add_shortcode('image_upload_tile', 'image_upload_tile_shortcode');
function image_upload_tile_shortcode() {
if (!is_user_logged_in()) return '<p>Please log in to upload images.</p>';
ob_start();
?>
<!-- Tile Trigger -->
<div class="upload-tile" data-bs-toggle="modal" data-bs-target="#cropperModal">
<span>+ Upload Image</span>
</div>
<!-- Bootstrap Modal -->
<div class="modal fade" id="cropperModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Crop & Upload</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Cropper Container -->
<div class="img-container">
<img id="cropper-image" src="" alt="">
</div>
<!-- Hidden File Input -->
<input type="file" id="image-input" accept="image/*" style="display: none;">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="upload-cropped">Upload</button>
</div>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
3. JavaScript (custom-cropper.js)
jQuery(document).ready(function($) {
let cropper;
const modal = new bootstrap.Modal(document.getElementById('cropperModal'));
// When tile is clicked, trigger hidden file input
$('.upload-tile').click(function() {
$('#image-input').click();
});
// When file is selected, initialize Cropper
$('#image-input').change(function(e) {
if (e.target.files.length) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function(event) {
$('#cropper-image').attr('src', event.target.result);
modal.show();
// Initialize Cropper after modal is shown
$('#cropperModal').on('shown.bs.modal', function() {
const image = document.getElementById('cropper-image');
cropper = new Cropper(image, {
aspectRatio: 1, // Square crop (adjust as needed)
viewMode: 1,
autoCropArea: 0.8,
});
});
};
reader.readAsDataURL(file);
}
});
// Handle upload
$('#upload-cropped').click(function() {
if (cropper) {
// Get cropped canvas
const canvas = cropper.getCroppedCanvas({
width: 800, // Set desired output width
height: 800, // Set desired output height
});
// Convert to Blob for AJAX upload
canvas.toBlob(function(blob) {
const formData = new FormData();
formData.append('action', 'handle_cropped_upload');
formData.append('cropped_image', blob, 'upload.jpg');
formData.append('security', my_ajax_object.nonce); // Localized nonce
$.ajax({
url: my_ajax_object.ajaxurl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
modal.hide();
alert('Upload successful!');
},
});
}, 'image/jpeg', 0.9); // 0.9 = quality (0-1)
}
});
// Destroy Cropper when modal closes
$('#cropperModal').on('hidden.bs.modal', function() {
if (cropper) {
cropper.destroy();
$('#image-input').val(''); // Reset file input
}
});
});
4. PHP AJAX Handler (in functions.php)
// Localize AJAX URL and nonce
add_action('wp_enqueue_scripts', 'localize_cropper_script');
function localize_cropper_script() {
wp_localize_script('custom-cropper', 'my_ajax_object', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('cropper_upload_nonce'),
]);
}
// Handle AJAX upload
add_action('wp_ajax_handle_cropped_upload', 'handle_cropped_upload');
function handle_cropped_upload() {
check_ajax_referer('cropper_upload_nonce', 'security');
if (!is_user_logged_in()) wp_send_json_error('Not logged in');
$user_id = get_current_user_id();
$upload = wp_upload_bits($_FILES['cropped_image']['name'], null, file_get_contents($_FILES['cropped_image']['tmp_name']));
if ($upload['error']) wp_send_json_error('Upload failed');
$attachment_id = wp_insert_attachment([
'post_mime_type' => $_FILES['cropped_image']['type'],
'post_title' => sanitize_file_name($_FILES['cropped_image']['name']),
'post_content' => '',
'post_status' => 'inherit',
], $upload['file']);
require_once(ABSPATH . 'wp-admin/includes/image.php');
wp_update_attachment_metadata($attachment_id, wp_generate_attachment_metadata($attachment_id, $upload['file']));
// Update user meta
update_user_meta($user_id, 'custom_cropped_image_id', $attachment_id);
wp_send_json_success();
}
Why Bootstrap Modal Works Well
- Built-in Responsiveness: Works on all devices.
- Focus Trap: Keeps user interaction within the cropper.
- Easy Trigger: Simple
data-bs-toggleattribute for the tile. - Clean Backdrop: Dims the rest of the page, focusing attention on cropping.
Alternative Approach
If you don’t want Bootstrap, use WordPress’s native wp_enqueue_media() + a <dialog> element, but Bootstrap provides better cross-browser support for modals.