Source Code

<div class="container py-5 d-flex justify-content-center">
  <div class="card bsavatar-card">
    <div class="card-body p-4 text-center">
      <div class="bsavatar-circle mb-3" id="bsavatarCircle">
        <span id="bsavatarInitials">PN</span>
        <img id="bsavatarImg" class="d-none" alt="Profile photo">
      </div>

      <input type="file" accept="image/*" class="d-none" id="bsavatarInput">
      <div class="d-flex justify-content-center gap-2">
        <button type="button" class="btn btn-sm btn-outline-secondary" id="bsavatarChange">Change photo</button>
        <button type="button" class="btn btn-sm btn-outline-danger d-none" id="bsavatarRemove">Remove</button>
      </div>

      <button type="button" class="btn btn-dark btn-sm fw-bold mt-3 d-none" id="bsavatarSave">Save changes</button>
      <p class="small text-muted mt-3 mb-0" id="bsavatarStatus">&nbsp;</p>
    </div>
  </div>
</div>

Bootstrap Avatar Upload Editor — Free HTML CSS JS Snippet

Bootstrap Avatar Upload Editor · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Initials and photo states are always kept mutually exclusive, toggled together on every change
The native file input is hidden and triggered via a normal styled button, a standard cross-browser technique
Save only appears once a real change has happened, not just because the file picker was opened
Every previous object URL is revoked before a new one is created, on both photo change and removal
Removing a photo correctly resets the file input's own value, so the same file can be re-selected afterward

About this UI Snippet

Bootstrap Avatar Upload Editor — HTML, CSS & JavaScript

Screenshot of the Bootstrap Avatar Upload Editor snippet rendered live

The circular avatar has two mutually exclusive states — an initials fallback and an actual photo — and this snippet keeps exactly one visible at all times by toggling both together every time either changes: choosing a photo hides #bsavatarInitials and shows #bsavatarImg, and Remove reverses both in the same click handler rather than leaving a stale image element hidden underneath.

A hidden native <input type="file"> is triggered programmatically via input.click() from the visible "Change photo" button — this is the standard technique for styling a file input as a normal button, since a real <input type="file"> can't be restyled directly across browsers with any real consistency.

Save only appears once markDirty() has actually run, called from both selecting a new photo and removing the existing one — so accidentally opening the file picker and cancelling it (which never fires the change event) correctly leaves the Save button hidden, since nothing was actually changed. The object URL is revoked before every new one is created, in change and again in Remove, so switching photos or removing one repeatedly never accumulates unreleased memory.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Hand this snippet to an AI coding assistant like Claude and ask it to add drag-and-drop support directly onto the avatar circle as an alternative to clicking "Change photo", or to add a confirmation step before Remove when a photo (not just initials) is currently showing, since removing a real uploaded photo is more consequential than clearing an empty state.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a Bootstrap 5.3 avatar upload editor, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble it.

Requirements:
- A circular avatar that shows a user's initials by default, and a hidden native file input triggered by a visible "Change photo" button.
- Selecting an image file swaps the circle to show that photo (using object-fit: cover) instead of the initials, and reveals a "Remove" button.
- Clicking "Remove" reverts the circle back to showing the initials, hides the Remove button again, and resets the file input so the same file could be re-selected.
- A "Save changes" button must stay hidden until either a new photo is selected or the photo is removed — opening and cancelling the file picker without selecting anything must not reveal it.
- Every previously created object URL must be revoked before a new one is created (on photo change) and when the photo is removed, so repeated changes never leak memory.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Load the snippetA circular avatar shows initials ("PN") with no Save button visible.
  2. 2
    Click "Change photo" and pick an imageThe circle swaps to your photo, a Remove button appears, and Save reveals itself.
  3. 3
    Click "Remove"The circle reverts to initials, Remove disappears again, and Save is still shown since something changed.
  4. 4
    Click "Save changes"A green confirmation appears and the Save button hides itself until the next real change.
  5. 5
    Open the file picker and cancel it without choosing a fileNothing changes and Save stays hidden, since no actual photo change occurred.

Real-world uses

Common Use Cases

Account and profile settings pages
The standard "change your profile picture" pattern used across nearly every account settings screen.
Team member and user management admin panels
Let an admin update a team member's avatar with the same clear save-when-dirty behavior.
Onboarding flows collecting a profile photo
Pairs with bootstrap-image-crop-before-upload for a flow that also lets the user reposition and zoom before saving.

Got questions?

Frequently Asked Questions

Native file inputs render very differently across browsers and resist consistent styling; hiding it and triggering .click() from a normal, fully-stylable button is the standard, reliable way to get a custom-looking upload trigger.

No — the change event only fires when a file is actually selected, so cancelling the picker leaves everything, including the hidden Save button, exactly as it was.

Yes. Keep the object URL and a dirty boolean in component state, revoking the previous URL inside the same state-setting logic used for a new selection, and trigger the hidden input via a ref's .click() method from your styled button.

Inside the Save handler, upload the actual File object (kept from the change event, not just its object URL) to your backend, and only show the success message once that request resolves — a real network request should also handle and surface a failure state.