Creates modals in WordPress admin using wp.media.view.Modal component. Use this skill when implementing modals, dialogs, or popup forms in WordPress admin areas. Includes proper setup, common pitfalls, AJAX tracking patterns, and complete working examples.
wp.media.view.Modal)Use this skill when you need to:
alert() in JavaScriptDo NOT use for:
@wordpress/components)confirm() or alert())See complete working examples in examples/ folder:
examples/php/Modal_Handler.phpexamples/js/modal.jsexamples/templates/modal-template.phpexamples/css/modal.css. Additional CSS is only needed for custom elements. The modal itself doesn’t need new CSS to work out of the box.wp.media.view.Modal for consistency with WordPress UIwp_enqueue_media(); // Loads wp.media, Backbone, Underscore
wp_enqueue_script(
'your-modal-script',
'path/to/modal.js',
[ 'jquery', 'media-views' ], // Required dependencies
VERSION,
true
);
This order MUST be followed:
// 1. Create controller with required methods
const modalController = _.extend({}, Backbone.Events, {
state: function() { return this; },
get: function() { return null; }
});
// 2. Create modal
const modal = new wp.media.view.Modal({ controller: modalController });
// 3. Open modal FIRST
modal.open();
// 4. THEN inject content
const content = wp.template('your-modal')({ data: 'value' });
modal.$el.find('.media-modal-content').html(content);
// 5. Attach event handlers
modal.$el.find('#save-button').on('click', saveHandler);
// 6. Cleanup on close
modal.on('close', function() { modal.remove(); });
⚠️ Common Mistake: Injecting content before modal.open() results in empty modal.
Error: Modal opens but shows no content
Cause: Content injected before modal.open()
Solution:
// ❌ WRONG ORDER
modal.$el.find('.media-modal-content').html(content);
modal.open(); // Too late!
// ✅ CORRECT ORDER
modal.open(); // First!
modal.$el.find('.media-modal-content').html(content); // Then inject
Cause: Controller missing required methods
Solution:
// ✅ Must include both state() and get()
const modalController = _.extend({}, Backbone.Events, {
state: function() { return this; },
get: function() { return null; }
});
Cause: Not waiting for async operations
Solution: Use callback pattern
modal.$el.find('#save').on('click', function() {
performAsyncSave(function onComplete() {
modal.close(); // Only close when done
});
});
See examples/js/modal.js for complete AJAX tracking implementation.
Full implementation in examples/js/modal.js.
Cause: Toolbar is for complex media frames, not simple modals
Solution: Use plain HTML buttons in template
<!-- ✅ CORRECT: Buttons in template content -->
<div class="modal-buttons">
<button class="button button-primary" id="save">Save</button>
<button class="button" id="cancel">Cancel</button>
</div>
See examples/templates/modal-template.php.
Always disable buttons during async operations:
$saveButton.prop('disabled', true).text('Saving...');
$cancelButton.prop('disabled', true);
$spinner.css('visibility', 'visible');
performSave(function() {
modal.close(); // Re-enable happens automatically on close
});
Avoid unnecessary AJAX requests:
if (currentValue !== newValue) {
element.value = newValue;
element.dispatchEvent(new Event('change'));
}
<# if ( data.showWarning ) { #>
<div class="warning">Warning message</div>
<# } #>
modal.on('close', function() {
modal.remove(); // Prevents stale data on next open
});
your-plugin/
├── admin/
│ ├── includes/
│ │ └── class-modal-handler.php (See examples/php/)
│ └── assets/
│ ├── js/
│ │ └── modal.js (See examples/js/)
│ └── css/
│ └── modal.css (See examples/css/)
└── templates/
└── modal-template.php (See examples/templates/)
When modal doesn't work, verify:
wp_enqueue_media() called?['jquery', 'media-views'] included?state() and get() methods?modal.open() called BEFORE content injection?tmpl-name → wp.template('name'))?modal.remove() called on close?.media-modal-content?