WordPress wp_enqueue_media(): Open the Media Library Picker on a Custom Admin Page

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.