How to display custom avatar and biography with PHP in WordPress

Contents

Overview

This tutorial shows, step-by-step, how to add and display a custom avatar (stored as a WordPress attachment) and a custom biography for users, using PHP (and a small bit of JavaScript to integrate the WordPress Media Library on profile pages). The solution:

  • adds custom fields to the user profile screen (avatar upload/select optional custom biography)
  • saves values securely using core hooks and capability checks
  • intercepts get_avatar() output so themes and plugins that call get_avatar() display your custom avatar with proper size and CSS classes
  • provides a template function and a shortcode to render avatar biography together
  • includes notes on sanitization, fallback behavior, performance, and troubleshooting.

Design choices and rationale

We store the avatar as an attachment ID in user meta (meta key: custom_avatar_id). Using an attachment ID is preferred to a raw URL because WordPress can generate responsive image markup, sizes and srcset, and it keeps media managed in the Media Library. The biography can use the built-in author description (description meta) or a separate meta key (example uses custom_bio).

Quick summary of hooks used

  • show_user_profile, edit_user_profile — render extra profile fields
  • personal_options_update, edit_user_profile_update — save profile fields
  • admin_enqueue_scripts — enqueue media scripts on profile pages
  • get_avatar — filter avatar output and return custom image when present
  • add_shortcode — provide a simple shortcode for placement in content.

Full implementation

1) Add profile fields (avatar and biography)

Add the following PHP to a plugin or your themes functions.php. It displays a profile field with a preview, an input holding the attachment ID, and an Upload / Select button. The code also enqueues the required media scripts and a small JS file to open the WordPress media modal.

lt?php
// 1. Show extra fields on profile screen
function custom_profile_fields_display( user ) {
    avatar_id = intval( get_user_meta( user-gtID, custom_avatar_id, true ) )
    avatar_url = avatar_id ? wp_get_attachment_image_url( avatar_id, thumbnail ) : 
    custom_bio = get_user_meta( user-gtID, custom_bio, true )
    ?>
    lth3gtCustom Avatar and Biographylt/h3gt
    lttable class=form-tablegt
        lttrgt
            ltthgtltlabel for=custom_avatargtCustom Avatarlt/labelgtlt/thgt
            lttdgt
                ltimg id=custom-avatar-preview src= style=max-width:100px height:auto display:>
                ltinput type=hidden name=custom_avatar_id id=custom-avatar-id value=gt
                ltbutton type=button class=button id=custom-avatar-uploadgtUpload / Selectlt/buttongt
                nbspltbutton type=button class=button id=custom-avatar-removegtRemovelt/buttongt
                ltp class=descriptiongtUpload or select an image from the media library. Recommended minimum: 150x150px.lt/pgt
            lt/tdgt
        lt/trgt
        lttrgt
            ltthgtltlabel for=custom_biogtCustom Biographylt/labelgtlt/thgt
            lttdgt
                lttextarea name=custom_bio id=custom_bio rows=5 cols=30gt

2) JavaScript to use the Media Library (admin profile pages)

Place this JavaScript in a file named custom-profile-media.js inside the same plugin folder (or adjust the path above). It opens the WP media frame, allows selecting one image, and writes the attachment ID preview.

(function(){
    var mediaFrame
    (#custom-avatar-upload).on(click, function(e){
        e.preventDefault()
        if (mediaFrame) {
            mediaFrame.open()
            return
        }
        mediaFrame = wp.media({
            title: Select or Upload Avatar,
            button: { text: Use this image },
            multiple: false
        })

        mediaFrame.on(select, function(){
            var attachment = mediaFrame.state().get(selection).first().toJSON()
            (#custom-avatar-id).val(attachment.id)
            (#custom-avatar-preview).attr(src, attachment.url).show()
        })

        mediaFrame.open()
    })

    (#custom-avatar-remove).on(click, function(e){
        e.preventDefault()
        (#custom-avatar-id).val()
        (#custom-avatar-preview).hide().attr(src, )
    })
})(jQuery)

3) Save profile fields securely

Use profile update hooks and capability checks. Sanitize the inputs before saving. We store avatar as integer (attachment ID) and bio as sanitized text.

lt?php
function custom_profile_fields_save( user_id ) {
    if ( ! current_user_can( edit_user, user_id ) ) {
        return false
    }

    // Avatar: accept only integer attachment IDs
    if ( isset( _POST[custom_avatar_id] ) ) {
        avatar_id = intval( _POST[custom_avatar_id] )
        if ( avatar_id ) {
            update_user_meta( user_id, custom_avatar_id, avatar_id )
        } else {
            delete_user_meta( user_id, custom_avatar_id )
        }
    }

    // Custom bio: sanitize text
    if ( isset( _POST[custom_bio] ) ) {
        custom_bio = wp_kses_post( _POST[custom_bio] ) // allow basic formatting
        if ( custom_bio ) {
            update_user_meta( user_id, custom_bio, custom_bio )
        } else {
            delete_user_meta( user_id, custom_bio )
        }
    }
}
add_action( personal_options_update, custom_profile_fields_save )
add_action( edit_user_profile_update, custom_profile_fields_save )
?gt

4) Filter get_avatar() to return the custom avatar

Intercepts calls to get_avatar() and returns an ltimggt element for users with a custom avatar. The filter preserves class attributes, alt text, and size. If no custom avatar exists, it returns the original avatar (Gravatar or other).

lt?php
function custom_get_avatar( avatar, id_or_email, size, default, alt ) {
    user = false

    if ( is_numeric( id_or_email ) ) {
        id = (int) id_or_email
        user = get_user_by( id, id )
    } elseif ( is_object( id_or_email ) ) {
        // WP_User or comment object
        if ( ! empty( id_or_email-gtuser_id ) ) {
            user = get_user_by( id, (int) id_or_email-gtuser_id )
        } elseif ( ! empty( id_or_email-gtuser_login ) ) {
            user = get_user_by( login, id_or_email-gtuser_login )
        }
    } else {
        user = get_user_by( email, id_or_email )
    }

    if ( ! user ) {
        return avatar // Not a user we can handle
    }

    avatar_id = intval( get_user_meta( user-gtID, custom_avatar_id, true ) )
    if ( ! avatar_id ) {
        return avatar // No custom avatar found, fallback
    }

    // Use wp_get_attachment_image to leverage image sizes and srcset
    size_attr = intval( size ) ? size : 96
    img = wp_get_attachment_image( avatar_id, array( size_attr, size_attr ), false, array(
        class => avatar avatar- . size_attr .  photo,
        alt   => esc_attr( alt ? alt : user-gtdisplay_name ),
    ) )

    if ( img ) {
        return img
    }

    return avatar
}
add_filter( get_avatar, custom_get_avatar, 10, 5 )
?gt

5) Template function and shortcode to display avatar and biography together

A helper function keeps code DRY. It accepts a user ID or WP_User, a size in pixels, and whether to echo or return the HTML. Theres also a simple shortcode [custom_avatar_bio user_id=1 size=96].

lt?php
function custom_avatar_bio_html( user_input = null, size = 96, echo = true ) {
    if ( is_null( user_input ) ) {
        global post
        if ( isset( post-gtpost_author ) ) {
            user = get_user_by( id, post-gtpost_author )
        } else {
            return 
        }
    } elseif ( is_numeric( user_input ) ) {
        user = get_user_by( id, (int) user_input )
    } elseif ( user_input instanceof WP_User ) {
        user = user_input
    } else {
        user = false
    }

    if ( ! user ) {
        return 
    }

    // Avatar: this will call the get_avatar filter and use custom avatar if available
    avatar_html = get_avatar( user->ID, size, , user->display_name )

    // Bio: prefer custom_bio, fallback to built-in description
    bio = get_user_meta( user->ID, custom_bio, true )
    if ( ! bio ) {
        bio = get_user_meta( user->ID, description, true )
    }
    bio = wpautop( wp_kses_post( bio ) )

    html  = ltdiv class=custom-avatar-biogt
    html .= ltdiv class=custom-avatar-wrapgt . avatar_html . lt/divgt
    if ( bio ) {
        html .= ltdiv class=custom-bio-wrapgt . bio . lt/divgt
    }
    html .= lt/divgt

    if ( echo ) {
        echo html
        return 
    }
    return html
}

// Shortcode: [custom_avatar_bio user_id=1 size=96]
function custom_avatar_bio_shortcode( atts ) {
    atts = shortcode_atts( array(
        user_id => ,
        size    => 96,
    ), atts, custom_avatar_bio )

    return custom_avatar_bio_html( atts[user_id], intval( atts[size] ), false )
}
add_shortcode( custom_avatar_bio, custom_avatar_bio_shortcode )
?gt

6) CSS for layout (simple example)

Add this CSS to your theme stylesheet to align avatar and biography side-by-side or stacked on small screens.

.custom-avatar-bio{
    display:flex
    align-items:flex-start
    gap:16px
}
.custom-avatar-wrap img.avatar{
    border-radius:50%
    display:block
}
.custom-bio-wrap{
    flex:1 1 auto
}
/ Responsive fallback /
@media (max-width:480px){
    .custom-avatar-bio{ flex-direction:column align-items:center text-align:center }
    .custom-bio-wrap{ text-align:left }
}

Security, sanitization and capability notes

  • Capability checks: We use current_user_can(edit_user, user_id) when saving profile fields. That prevents unauthorized modifications.
  • Sanitization: Avatar: saved as integer via intval() to ensure only attachment IDs are stored. Biography: sanitized with wp_kses_post() to allow basic HTML, or use sanitize_textarea_field() for plain text.
  • File validation: The media modal only allows selecting existing media attachments. If you accept direct URLs instead of attachment IDs, validate with esc_url_raw() and consider checking file type or host restrictions.
  • Escaping for output: get_avatar and wp_get_attachment_image produce safe HTML when printing custom strings like bios, use wp_kses_post() and wpautop() or esc_html() depending on whether you allow markup.

Performance and caching

  • Using attachment IDs and wp_get_attachment_image lets WordPress handle responsive images (srcset) and size variations, which is optimal for performance.
  • If you expect many avatar lookups (e.g., on archive pages), consider object caching user meta (WordPress does some caching by default). Avoid expensive operations inside the get_avatar filter.
  • If you build a high-traffic site and need to serve many avatars, consider generating and serving a dedicated sized image with proper caching headers.

Compatibility and fallbacks

  • If no custom avatar is set, the filter returns the original avatar (which might be a Gravatar or a theme-provided fallback).
  • The solution works with core WordPress functions and hooks and is compatible with themes or plugins that use get_avatar().
  • Some plugins replace get_avatar behavior or add caching layers if you see unexpected results, check plugin priority and conflicts. You can adjust the filter priority (default 10) or remove conflicting filters.

Troubleshooting common problems

  1. No media modal appears on profile page — ensure wp_enqueue_media() is called and that your admin_enqueue_scripts hook runs only on profile.php or user-edit.php. Also confirm your JS path is correct and the file loads (check browser console/network).
  2. Custom avatar not displaying — check that custom_avatar_id is stored in usermeta and that the attachment ID still exists in the Media Library. Inspect the markup returned by get_avatar (use view source / inspector).
  3. Broken image size / blurry image — ensure you requested an appropriate size when calling get_avatar() or in the template helper. If the attachment lacks the requested size, WP will serve the closest size.
  4. Biography formatting lost — ensure you used wp_kses_post() / wpautop() appropriately. If you passed sanitize_text_field(), markup will be stripped.

Alternative approaches

  • Store the avatar as a URL in user meta. Simpler but loses WP image resizing and srcset benefits.
  • Use a plugin that manages avatars (for example, search the plugin repository). Plugins may include bulk import, remote avatars, or avatar permissions.
  • If you only need to display custom bio but not an avatar, you can skip the media uploader and just add a textarea to the profile screen.

References

Final notes

This implementation provides a maintainable and secure method to let users have custom avatars and biographies. It integrates with WordPress core media handling, keeps media in the Media Library, supports responsive images, and exposes a small helper plus shortcode for easy placement within themes and content.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *