WordPress’s Media Library modal — the dialog that slides in when you click “Add Media” on a post editing screen — is a full Backbone.js application called the Media Frame. It handles file uploads, browsing, filtering, and selection, and it is entirely reusable outside of the default post editing context. The wp_enqueue_media() function loads all the JavaScript and CSS that the Media Frame needs, and a few lines of JavaScript are enough to open it from any button click on a custom admin page or meta box. This is the correct way to add a media picker to a plugin settings page or a custom meta box — no third-party library needed, and the result is the identical UI that users already know from native WordPress editing. Without wp_enqueue_media(), the modal simply does not load and wp.media is undefined.
Problem: Your plugin's settings page has an "Upload Image" button that should open the WordPress Media Library so the user can select or upload an image — and have the selected image URL or ID saved in a hidden form field.
Solution: Call wp_enqueue_media() in the admin_enqueue_scripts hook (scoped to your admin page), render a button and hidden input in the settings form, then open and handle the wp.media frame in JavaScript.
<?php
// ── Enqueue media only on your admin page ────────────────────────────
add_action( 'admin_enqueue_scripts', 'my_plugin_enqueue_media_picker' );
function my_plugin_enqueue_media_picker( string $hook ) {
// Only load on your settings page — avoids loading on every admin page
if ( 'settings_page_my-plugin' !== $hook ) {
return;
}
wp_enqueue_media(); // loads all Media Library JS + CSS
wp_enqueue_script(
'my-plugin-media-picker',
plugin_dir_url( __FILE__ ) . 'js/media-picker.js',
[ 'jquery' ],
'1.0.0',
true
);
}
// ── Settings page HTML ────────────────────────────────────────────────
function render_my_plugin_settings() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Forbidden.' );
}
$image_id = (int) get_option( 'my_plugin_header_image_id', 0 );
$image_url = $image_id ? wp_get_attachment_image_url( $image_id, 'medium' ) : '';
?>
<div class="wrap">
<h1><?php esc_html_e( 'My Plugin Settings', 'textdomain' ); ?></h1>
<form method="post" action="options.php">
<?php settings_fields( 'my_plugin_settings_group' ); ?>
<table class="form-table">
<tr>
<th><?php esc_html_e( 'Header Image', 'textdomain' ); ?></th>
<td>
<!-- Preview -->
<img id="my-plugin-image-preview"
src="<?php echo esc_url( $image_url ); ?>"
style="max-width:200px;display:<?php echo $image_url ? 'block' : 'none'; ?>;">
<!-- Hidden field stores the attachment ID -->
<input type="hidden"
id="my-plugin-image-id"
name="my_plugin_header_image_id"
value="<?php echo esc_attr( $image_id ); ?>">
<button type="button" id="my-plugin-open-media" class="button">
<?php esc_html_e( 'Choose Image', 'textdomain' ); ?>
</button>
<button type="button" id="my-plugin-remove-media" class="button"
style="<?php echo $image_url ? '' : 'display:none;'; ?>">
<?php esc_html_e( 'Remove', 'textdomain' ); ?>
</button>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
JavaScript file js/media-picker.js:
( function ( $ ) {
'use strict';
var mediaFrame;
$( '#my-plugin-open-media' ).on( 'click', function ( e ) {
e.preventDefault();
// Reuse existing frame if already opened
if ( mediaFrame ) {
mediaFrame.open();
return;
}
mediaFrame = wp.media( {
title: 'Select or Upload Header Image',
button: { text: 'Use this image' },
library: { type: 'image' },
multiple: false,
} );
mediaFrame.on( 'select', function () {
var attachment = mediaFrame.state().get( 'selection' ).first().toJSON();
$( '#my-plugin-image-id' ).val( attachment.id );
$( '#my-plugin-image-preview' ).attr( 'src', attachment.url ).show();
$( '#my-plugin-remove-media' ).show();
} );
mediaFrame.open();
} );
$( '#my-plugin-remove-media' ).on( 'click', function () {
$( '#my-plugin-image-id' ).val( '' );
$( '#my-plugin-image-preview' ).attr( 'src', '' ).hide();
$( this ).hide();
} );
} )( jQuery );
NOTE: wp_enqueue_media() accepts an optional $args array with a 'post' key — passing the current post ID (wp_enqueue_media( [ 'post' => $post_id ] )) ensures that the Media Frame shows the attachments already uploaded to that post in the "Uploaded to this post" filter tab. Omitting it shows all media. Always scope wp_enqueue_media() to your specific admin page using the $hook parameter — loading it on every admin page adds roughly 30–40 HTTP requests and significant JavaScript weight to pages that have no media picker.