Add Custom Fields to the WordPress User Profile with User Meta

WordPress stores user-specific data in the wp_usermeta table using the same key-value structure as post meta. There are built-in meta keys for display name, description, and social URLs, but the system is fully extensible: you can add any meta key you like with add_user_meta() or update_user_meta(), and expose it as a field in the user profile editor using the show_user_profile and edit_user_profile action hooks. Custom user meta is useful for: storing user preferences (preferred language, notification settings), extending the profile with business-specific fields (department, employee ID, phone number), building author profile pages with richer data, and tracking user-specific state (onboarding progress, last-seen post ID). The save logic uses personal_options_update and edit_user_profile_update hooks — both fire when the user profile form is saved, covering both the user editing their own profile and an administrator editing any profile.

Problem: You need to add custom fields to the WordPress user profile — for example a phone number, job title, or department — and save the values to wp_usermeta securely.

Solution: Display custom fields via show_user_profile / edit_user_profile, save with personal_options_update / edit_user_profile_update. Read and write meta with get_user_meta() and update_user_meta().

<?php
// ── Display custom fields on the profile form ────────────────────────────
add_action( 'show_user_profile',   'show_custom_user_fields' );
add_action( 'edit_user_profile',   'show_custom_user_fields' );

function show_custom_user_fields( $user ) {
    $phone  = esc_attr( get_user_meta( $user->ID, 'phone_number', true ) );
    $dept   = esc_attr( get_user_meta( $user->ID, 'department',   true ) );
    ?>
    <h3><?php esc_html_e( 'Additional Information', 'textdomain' ); ?></h3>
    <table class="form-table">
        <tr>
            <th><label for="phone_number"><?php esc_html_e( 'Phone', 'textdomain' ); ?></label></th>
            <td>
                <input type="tel" name="phone_number" id="phone_number"
                       value="<?php echo $phone; ?>" class="regular-text">
            </td>
        </tr>
        <tr>
            <th><label for="department"><?php esc_html_e( 'Department', 'textdomain' ); ?></label></th>
            <td>
                <input type="text" name="department" id="department"
                       value="<?php echo $dept; ?>" class="regular-text">
            </td>
        </tr>
    </table>
    <?php
}

// ── Save custom fields ────────────────────────────────────────────────────
add_action( 'personal_options_update',  'save_custom_user_fields' );
add_action( 'edit_user_profile_update', 'save_custom_user_fields' );

function save_custom_user_fields( $user_id ) {
    // Verify the current user can edit this profile
    if ( ! current_user_can( 'edit_user', $user_id ) ) {
        return false;
    }

    update_user_meta( $user_id, 'phone_number',
        sanitize_text_field( $_POST['phone_number'] ?? '' ) );

    update_user_meta( $user_id, 'department',
        sanitize_text_field( $_POST['department'] ?? '' ) );
}

// ── Read anywhere ────────────────────────────────────────────────────────
$phone = get_user_meta( get_current_user_id(), 'phone_number', true );

// Get all meta for a user (returns array of arrays)
$all_meta = get_user_meta( $user_id );

// Query users by meta value
$editors_in_sales = get_users( [
    'role'       => 'editor',
    'meta_key'   => 'department',
    'meta_value' => 'Sales',
] );

NOTE: update_user_meta() creates the meta key if it does not exist and updates it if it does — there is no need to call add_user_meta() first unless you specifically want multiple values for the same key (add_user_meta() with $unique = false). Always validate with current_user_can( 'edit_user', $user_id ) before saving — without this check, any logged-in user who can reach the profile save endpoint could overwrite another user's meta.