Meta Boxes API

Meta boxes are draggable content boxes that appear on edit screens in WordPress admin. They’re used for custom fields, taxonomies, and additional post/page options.

Core Functions

add_meta_box()

Adds a meta box to an edit screen.

add_meta_box(
    string            $id,
    string            $title,
    callable          $callback,
    string|array|null $screen = null,
    string            $context = 'advanced',
    string            $priority = 'default',
    array             $callback_args = null
);

Parameters:

Parameter Type Description
$id string Unique ID for the meta box
$title string Title displayed in the meta box header
$callback callable Function that outputs the meta box content
$screen string/array/null Screen(s) to show the meta box on
$context string ‘normal’, ‘side’, or ‘advanced’
$priority string ‘high’, ‘core’, ‘default’, or ‘low’
$callback_args array Data passed to the callback

remove_meta_box()

Removes a meta box.

remove_meta_box( string $id, string|array|WP_Screen $screen, string $context );

do_meta_boxes()

Outputs meta boxes for a screen.

do_meta_boxes( string|WP_Screen $screen, string $context, mixed $data_object );

Basic Meta Box

add_action( 'add_meta_boxes', 'my_custom_meta_box' );

function my_custom_meta_box() {
    add_meta_box(
        'my_custom_meta_box',           // ID
        __( 'My Custom Meta Box' ),     // Title
        'my_custom_meta_box_callback',  // Callback
        'post',                         // Screen (post type)
        'normal',                       // Context
        'high'                          // Priority
    );
}

function my_custom_meta_box_callback( $post ) {
    // Add nonce for security
    wp_nonce_field( 'my_custom_meta_box', 'my_custom_meta_box_nonce' );
    
    // Get existing value
    $value = get_post_meta( $post->ID, '_my_custom_field', true );
    
    // Output form fields
    ?>
    <p>
        <label for="my_custom_field"><?php _e( 'Custom Field:' ); ?></label>
        <input type="text" id="my_custom_field" name="my_custom_field" 
               value="<?php echo esc_attr( $value ); ?>" class="widefat">
    </p>
    <?php
}

// Save the meta box data
add_action( 'save_post', 'my_save_custom_meta_box' );

function my_save_custom_meta_box( $post_id ) {
    // Verify nonce
    if ( ! isset( $_POST['my_custom_meta_box_nonce'] ) ) {
        return;
    }
    
    if ( ! wp_verify_nonce( $_POST['my_custom_meta_box_nonce'], 'my_custom_meta_box' ) ) {
        return;
    }
    
    // Check autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    
    // Check permissions
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }
    
    // Sanitize and save
    if ( isset( $_POST['my_custom_field'] ) ) {
        $value = sanitize_text_field( $_POST['my_custom_field'] );
        update_post_meta( $post_id, '_my_custom_field', $value );
    }
}

Contexts and Priorities

Contexts

Context Description
normal Below the editor (main column)
side Sidebar column
advanced Below normal context

Priorities

Priority Description
high Displayed first
core Core WordPress meta boxes
default Standard priority
low Displayed last

Multiple Post Types

add_action( 'add_meta_boxes', 'add_to_multiple_types' );

function add_to_multiple_types() {
    $screens = array( 'post', 'page', 'my_custom_type' );
    
    foreach ( $screens as $screen ) {
        add_meta_box(
            'shared_meta_box',
            __( 'Shared Settings' ),
            'shared_meta_box_callback',
            $screen,
            'side',
            'default'
        );
    }
}

// Or use array syntax
add_meta_box(
    'shared_meta_box',
    __( 'Shared Settings' ),
    'shared_meta_box_callback',
    array( 'post', 'page', 'my_custom_type' ),
    'side'
);

Complex Meta Box Example

add_action( 'add_meta_boxes', 'product_meta_boxes' );

function product_meta_boxes() {
    add_meta_box(
        'product_details',
        __( 'Product Details' ),
        'product_details_callback',
        'product',
        'normal',
        'high'
    );
}

function product_details_callback( $post ) {
    wp_nonce_field( 'product_details', 'product_details_nonce' );
    
    // Get meta values
    $price    = get_post_meta( $post->ID, '_product_price', true );
    $sku      = get_post_meta( $post->ID, '_product_sku', true );
    $stock    = get_post_meta( $post->ID, '_product_stock', true );
    $featured = get_post_meta( $post->ID, '_product_featured', true );
    ?>
    
    <table class="form-table">
        <tr>
            <th><label for="product_price"><?php _e( 'Price' ); ?></label></th>
            <td>
                <input type="number" id="product_price" name="product_price" 
                       value="<?php echo esc_attr( $price ); ?>" 
                       step="0.01" min="0" class="regular-text">
            </td>
        </tr>
        <tr>
            <th><label for="product_sku"><?php _e( 'SKU' ); ?></label></th>
            <td>
                <input type="text" id="product_sku" name="product_sku" 
                       value="<?php echo esc_attr( $sku ); ?>" class="regular-text">
            </td>
        </tr>
        <tr>
            <th><label for="product_stock"><?php _e( 'Stock' ); ?></label></th>
            <td>
                <input type="number" id="product_stock" name="product_stock" 
                       value="<?php echo esc_attr( $stock ); ?>" min="0">
            </td>
        </tr>
        <tr>
            <th><label for="product_featured"><?php _e( 'Featured' ); ?></label></th>
            <td>
                <label>
                    <input type="checkbox" id="product_featured" name="product_featured" 
                           value="1" <?php checked( $featured, '1' ); ?>>
                    <?php _e( 'Mark as featured product' ); ?>
                </label>
            </td>
        </tr>
    </table>
    
    <?php
}

add_action( 'save_post_product', 'save_product_details' );

function save_product_details( $post_id ) {
    if ( ! isset( $_POST['product_details_nonce'] ) ) {
        return;
    }
    
    if ( ! wp_verify_nonce( $_POST['product_details_nonce'], 'product_details' ) ) {
        return;
    }
    
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }
    
    // Save fields
    if ( isset( $_POST['product_price'] ) ) {
        update_post_meta( $post_id, '_product_price', floatval( $_POST['product_price'] ) );
    }
    
    if ( isset( $_POST['product_sku'] ) ) {
        update_post_meta( $post_id, '_product_sku', sanitize_text_field( $_POST['product_sku'] ) );
    }
    
    if ( isset( $_POST['product_stock'] ) ) {
        update_post_meta( $post_id, '_product_stock', absint( $_POST['product_stock'] ) );
    }
    
    $featured = isset( $_POST['product_featured'] ) ? '1' : '0';
    update_post_meta( $post_id, '_product_featured', $featured );
}

Removing Core Meta Boxes

add_action( 'admin_menu', 'remove_meta_boxes' );

function remove_meta_boxes() {
    // Post meta boxes
    remove_meta_box( 'submitdiv', 'post', 'side' );         // Publish box
    remove_meta_box( 'commentsdiv', 'post', 'normal' );     // Comments
    remove_meta_box( 'revisionsdiv', 'post', 'normal' );    // Revisions
    remove_meta_box( 'authordiv', 'post', 'normal' );       // Author
    remove_meta_box( 'slugdiv', 'post', 'normal' );         // Slug
    remove_meta_box( 'postexcerpt', 'post', 'normal' );     // Excerpt
    remove_meta_box( 'trackbacksdiv', 'post', 'normal' );   // Trackbacks
    remove_meta_box( 'postcustom', 'post', 'normal' );      // Custom Fields
    remove_meta_box( 'commentstatusdiv', 'post', 'normal' ); // Discussion
    remove_meta_box( 'postimagediv', 'post', 'side' );      // Featured Image
    remove_meta_box( 'tagsdiv-post_tag', 'post', 'side' );  // Tags
    remove_meta_box( 'categorydiv', 'post', 'side' );       // Categories
    
    // Page meta boxes
    remove_meta_box( 'pageparentdiv', 'page', 'side' );     // Page Attributes
    
    // Dashboard meta boxes
    remove_meta_box( 'dashboard_quick_press', 'dashboard', 'side' );
    remove_meta_box( 'dashboard_primary', 'dashboard', 'side' );
}

Callback Arguments

Pass data to your callback function:

add_meta_box(
    'my_meta_box',
    __( 'My Meta Box' ),
    'my_meta_box_callback',
    'post',
    'normal',
    'default',
    array(
        'options' => array( 'a', 'b', 'c' ),
        'default' => 'a',
    )
);

function my_meta_box_callback( $post, $meta_box ) {
    $args    = $meta_box['args'];
    $options = $args['options'];
    $default = $args['default'];
    
    // Use $options and $default in your output
}

Meta Boxes with Media Uploader

add_action( 'add_meta_boxes', 'image_upload_meta_box' );
add_action( 'admin_enqueue_scripts', 'enqueue_media_uploader' );

function enqueue_media_uploader( $hook ) {
    if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
        return;
    }
    
    wp_enqueue_media();
    wp_enqueue_script(
        'my-meta-box-image',
        plugin_dir_url( __FILE__ ) . 'js/meta-box-image.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );
}

function image_upload_meta_box() {
    add_meta_box(
        'custom_image_box',
        __( 'Custom Image' ),
        'custom_image_callback',
        'post',
        'side'
    );
}

function custom_image_callback( $post ) {
    wp_nonce_field( 'custom_image_box', 'custom_image_nonce' );
    
    $image_id = get_post_meta( $post->ID, '_custom_image_id', true );
    $image    = $image_id ? wp_get_attachment_image( $image_id, 'thumbnail' ) : '';
    ?>
    
    <div class="custom-image-container">
        <?php echo $image; ?>
    </div>
    
    <input type="hidden" name="custom_image_id" id="custom_image_id" 
           value="<?php echo esc_attr( $image_id ); ?>">
    
    <p>
        <button type="button" class="button" id="upload_image_button">
            <?php _e( 'Select Image' ); ?>
        </button>
        <button type="button" class="button" id="remove_image_button" 
                style="<?php echo $image_id ? '' : 'display:none'; ?>">
            <?php _e( 'Remove Image' ); ?>
        </button>
    </p>
    
    <?php
}

Hooks

Actions

Hook Description
add_meta_boxes Fires when meta boxes should be added
add_meta_boxes_{post_type} Post type specific
do_meta_boxes Fires before meta boxes are output

Filters

Filter Description
postbox_classes_{screen_id}_{meta_box_id} Filter meta box CSS classes
default_hidden_meta_boxes Set default hidden meta boxes
hidden_meta_boxes Filter hidden meta boxes

Block Editor Compatibility

For Gutenberg compatibility, use the __block_editor_compatible_meta_box flag:

add_meta_box(
    'my_classic_meta_box',
    __( 'Classic Meta Box' ),
    'my_classic_callback',
    'post',
    'side',
    'default',
    array(
        '__block_editor_compatible_meta_box' => true,
        '__back_compat_meta_box'             => false,
    )
);

Or use register_post_meta() for block editor native handling:

register_post_meta( 'post', '_my_custom_field', array(
    'show_in_rest'  => true,
    'single'        => true,
    'type'          => 'string',
    'auth_callback' => function() {
        return current_user_can( 'edit_posts' );
    },
) );