Work Services Journal About Contact
Warsaw Contact

Custom WordPress User Avatars Without a Plugin

Gravatar is a fine default until a client does not have one set up, or does not want a third-party service holding their photo at all, and WordPress falls back to a generic grey silhouette. Installing a whole plugin just to let someone upload a staff photo always felt like overkill for something this small. Here is the snippet I use instead: a profile picture field on the user edit screen, backed by the media library, that overrides Gravatar everywhere, including Elementor's dynamic author image widget.

How it works

The snippet adds an upload field under a "Profile Picture" section on the WordPress user profile page, using the native media uploader so there is nothing new to learn. The chosen image is stored as a normal media attachment and its ID saved to user meta. From there it hooks into the get_avatar filter, which is what every theme and plugin calls to render an avatar, so the uploaded image takes over globally with no per-theme work. A second pair of filters on get_the_author_meta and get_avatar_url makes sure Elementor's author widget, which reads the avatar slightly differently, picks it up too.

The code

Add this as a new PHP snippet via the Code Snippets plugin, not in functions.php:

<?php
/**
 * Custom User Profile Avatar (Replaces Gravatar + Works with Elementor)
 */

// === 1. Add custom avatar field ===
add_action( 'show_user_profile', 'custom_user_avatar_field' );
add_action( 'edit_user_profile', 'custom_user_avatar_field' );
function custom_user_avatar_field( $user ) {
    $avatar_id = get_user_meta( $user->ID, 'custom_user_avatar_id', true );
    $avatar_url = $avatar_id ? wp_get_attachment_image_url( $avatar_id, 'thumbnail' ) : '';
    ?>
    <div id="custom-avatar-field" style="display:none;">
        <h3>Profile Picture</h3>
        <table class="form-table">
            <tr>
                <th><label for="custom_user_avatar">Profile Image</label></th>
                <td>
                    <img id="custom_user_avatar_preview" src="<?php echo esc_url( $avatar_url ); ?>" style="height:70px;width:70px;border-radius:50%;background:#f0f0f0;display:block;margin-bottom:10px;">
                    <input type="hidden" name="custom_user_avatar_id" id="custom_user_avatar" value="<?php echo esc_attr( $avatar_id ); ?>">
                    <button type="button" class="button" id="custom_user_avatar_button">Upload / Select Image</button>
                    <button type="button" class="button" id="custom_user_avatar_remove" style="margin-left:10px;">Remove</button>
                    <p class="description">Upload or select an avatar for this user.</p>
                </td>
            </tr>
        </table>
    </div>
    <?php
}

// === 2. Save avatar field & store URL for Elementor ===
add_action( 'personal_options_update', 'custom_user_avatar_save' );
add_action( 'edit_user_profile_update', 'custom_user_avatar_save' );
function custom_user_avatar_save( $user_id ) {
    if ( ! current_user_can( 'edit_user', $user_id ) ) return;
    $avatar_id = intval( $_POST['custom_user_avatar_id'] );
    update_user_meta( $user_id, 'custom_user_avatar_id', $avatar_id );
    $url = $avatar_id ? wp_get_attachment_image_url( $avatar_id, 'thumbnail' ) : '';
    update_user_meta( $user_id, 'profile_picture', $url ); // Elementor & others use this
}

// === 3. Enqueue scripts & reposition field ===
add_action( 'admin_enqueue_scripts', function( $hook ) {
    if ( in_array( $hook, ['profile.php', 'user-edit.php'] ) ) {
        wp_enqueue_media();
        $js = <<<JS
jQuery(document).ready(function($){
    $('#description').closest('tr').after( $('#custom-avatar-field').show().find('table tr') );
    $('h2:contains("Profile Picture")').closest('table').hide();
    var frame;
    $('#custom_user_avatar_button').on('click', function(e){
        e.preventDefault();
        if (frame) { frame.open(); return; }
        frame = wp.media({ title: 'Select or Upload Avatar', button: { text: 'Use this image' }, multiple: false });
        frame.on('select', function(){
            var attachment = frame.state().get('selection').first().toJSON();
            $('#custom_user_avatar').val(attachment.id);
            $('#custom_user_avatar_preview').attr('src', attachment.sizes.thumbnail.url);
        });
        frame.open();
    });
    $('#custom_user_avatar_remove').on('click', function(){
        $('#custom_user_avatar').val('');
        $('#custom_user_avatar_preview').attr('src', '');
    });
});
JS;
        wp_add_inline_script( 'jquery-core', $js );
    }
});

// === 4. Override get_avatar() globally ===
add_filter( 'get_avatar', function( $avatar, $id_or_email, $size, $default, $alt ) {
    $user = false;
    if ( is_numeric( $id_or_email ) ) {
        $user = get_user_by( 'id', absint( $id_or_email ) );
    } elseif ( is_object( $id_or_email ) && ! empty( $id_or_email->user_id ) ) {
        $user = get_user_by( 'id', absint( $id_or_email->user_id ) );
    } elseif ( is_email( $id_or_email ) ) {
        $user = get_user_by( 'email', $id_or_email );
    }
    if ( $user ) {
        $avatar_id = get_user_meta( $user->ID, 'custom_user_avatar_id', true );
        if ( $avatar_id ) {
            return wp_get_attachment_image( $avatar_id, [$size,$size], false, [
                'class' => 'custom-user-avatar',
                'alt'   => esc_attr( $alt ),
                'style' => "border-radius:50%;width:{$size}px;height:{$size}px;"
            ]);
        }
    }
    return $avatar;
}, 10, 5 );

// === 5. Make Elementor & others use our avatar URL when fetching meta ===
add_filter( 'get_the_author_meta', function( $value, $field, $user_id ) {
    if ( in_array( $field, ['profile_picture', 'user_avatar', 'avatar'] ) ) {
        $avatar_id = get_user_meta( $user_id, 'custom_user_avatar_id', true );
        if ( $avatar_id ) {
            return wp_get_attachment_image_url( $avatar_id, 'thumbnail' );
        }
    }
    return $value;
}, 10, 3 );

// === 6. Override get_avatar_url() so Elementor uses our custom avatar URL ===
add_filter( 'get_avatar_url', function( $url, $id_or_email, $args ) {
    $user = false;
    if ( is_numeric( $id_or_email ) ) {
        $user = get_user_by( 'id', absint( $id_or_email ) );
    } elseif ( is_object( $id_or_email ) && ! empty( $id_or_email->user_id ) ) {
        $user = get_user_by( 'id', absint( $id_or_email->user_id ) );
    } elseif ( is_email( $id_or_email ) ) {
        $user = get_user_by( 'email', $id_or_email );
    }
    if ( $user ) {
        $avatar_id = get_user_meta( $user->ID, 'custom_user_avatar_id', true );
        if ( $avatar_id ) {
            return wp_get_attachment_image_url( $avatar_id, 'thumbnail' );
        }
    }
    return $url;
}, 10, 3 );

What you get

A "Profile Picture" field appears on every user's edit screen with an upload and remove button. Once set, that image replaces the Gravatar fallback everywhere WordPress or Elementor asks for an avatar, no external request, no generic placeholder, no extra plugin dependency to keep updated.

Full source on GitHub: djaysan/Custom-User-Profile-Picture-No-Plugin-. Need this wired into a site build? Get in touch.

Let’s build

Have a project in mind?

Get in touch