Mobile Edit Profile Screen — Free HTML CSS JS Snippet
Mobile Edit Profile Screen · Mobile · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="mpe-phone">
<div class="mpe-screen">
<div class="mpe-status"><span>9:41</span><span class="mpe-batt"><i></i></span></div>
<header class="mpe-head">
<button class="mpe-cancel" id="mpeCancel">Cancel</button>
<h1>Edit Profile</h1>
<button class="mpe-save" id="mpeSave" disabled>Save</button>
</header>
<form class="mpe-form" id="mpeForm">
<div class="mpe-avatar-wrap">
<div class="mpe-avatar" id="mpeAvatar">JM</div>
<label class="mpe-avatar-edit" for="mpeFile">📷</label>
<input type="file" id="mpeFile" accept="image/*" hidden>
</div>
<div class="mpe-field">
<label>Full name</label>
<input type="text" id="mpeName" value="Jordan Mireles" required>
<p class="mpe-err" id="mpeNameErr" hidden>Name is required.</p>
</div>
<div class="mpe-field">
<label>Username</label>
<div class="mpe-prefix-input">
<span>@</span>
<input type="text" id="mpeUsername" value="jordan.m">
</div>
<p class="mpe-hint" id="mpeUserHint">Available</p>
</div>
<div class="mpe-field">
<label>Bio</label>
<textarea id="mpeBio" maxlength="140" rows="3">Product designer. Coffee, trail running, and side projects.</textarea>
<p class="mpe-count"><span id="mpeBioCount">62</span>/140</p>
</div>
<div class="mpe-field">
<label>Email</label>
<input type="email" id="mpeEmail" value="[email protected]" required>
<p class="mpe-err" id="mpeEmailErr" hidden>Enter a valid email address.</p>
</div>
<div class="mpe-field">
<label>Phone</label>
<input type="tel" id="mpePhone" value="+1 (555) 019-4482">
</div>
</form>
<p class="mpe-toast" id="mpeToast" hidden>✓ Profile updated</p>
</div>
</div>*{box-sizing:border-box;margin:0;padding:0}
html{scrollbar-width:none;-ms-overflow-style:none}
html::-webkit-scrollbar{display:none}
body{font-family:system-ui,-apple-system,sans-serif;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px;scrollbar-width:none;-ms-overflow-style:none}
body::-webkit-scrollbar{display:none}
.mpe-phone{position:relative;width:288px;height:600px;background:#0b1220;border-radius:46px;padding:12px;box-shadow:0 30px 60px -20px rgba(0,0,0,.6),inset 0 0 0 2px #1e293b}
.mpe-screen{width:100%;height:100%;border-radius:34px;overflow:hidden;background:#fff;color:#0f172a;display:flex;flex-direction:column}
.mpe-status{display:flex;justify-content:space-between;align-items:center;padding:13px 24px 0;font-size:13px;font-weight:700}
.mpe-batt{width:22px;height:11px;border:1.4px solid currentColor;border-radius:3px;position:relative;display:inline-block}
.mpe-batt::after{content:'';position:absolute;right:-3px;top:3px;width:2px;height:5px;background:currentColor;border-radius:0 1px 1px 0}
.mpe-batt i{position:absolute;left:1.4px;top:1.4px;bottom:1.4px;width:80%;background:currentColor;border-radius:1px}
.mpe-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px 12px;border-bottom:1px solid #f1f5f9}
.mpe-cancel,.mpe-save{background:none;border:none;font-size:13.5px;font-weight:700;cursor:pointer;font-family:inherit;padding:4px}
.mpe-cancel{color:#64748b}
.mpe-save{color:#6366f1}
.mpe-save:disabled{color:#c7d2fe;cursor:not-allowed}
.mpe-head h1{font-size:15px;font-weight:800}
.mpe-form{flex:1;overflow-y:auto;padding:20px 20px 30px;scrollbar-width:none;-ms-overflow-style:none}
.mpe-form::-webkit-scrollbar{display:none}
.mpe-avatar-wrap{position:relative;width:74px;height:74px;margin:0 auto 26px}
.mpe-avatar{width:74px;height:74px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#ec4899);color:#fff;font-weight:800;font-size:22px;display:flex;align-items:center;justify-content:center}
.mpe-avatar-edit{position:absolute;bottom:-2px;right:-2px;width:26px;height:26px;background:#0f172a;border:2px solid #fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;cursor:pointer}
.mpe-field{margin-bottom:18px}
.mpe-field label{display:block;font-size:11.5px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.4px;margin-bottom:7px}
.mpe-field input,.mpe-field textarea{width:100%;border:1.5px solid #e2e8f0;border-radius:11px;padding:11px 13px;font-size:13.5px;font-family:inherit;color:#0f172a;outline:none;transition:border-color .15s;resize:none}
.mpe-field input:focus,.mpe-field textarea:focus{border-color:#6366f1}
.mpe-field input.invalid{border-color:#ef4444}
.mpe-prefix-input{display:flex;align-items:center;border:1.5px solid #e2e8f0;border-radius:11px;padding:0 13px;transition:border-color .15s}
.mpe-prefix-input:focus-within{border-color:#6366f1}
.mpe-prefix-input span{color:#94a3b8;font-size:13.5px}
.mpe-prefix-input input{border:none;padding:11px 0 11px 3px}
.mpe-prefix-input input:focus{border:none}
.mpe-hint{font-size:11.5px;color:#16a34a;font-weight:600;margin-top:6px}
.mpe-hint.taken{color:#ef4444}
.mpe-err{font-size:11.5px;color:#ef4444;font-weight:600;margin-top:6px}
.mpe-count{text-align:right;font-size:11px;color:#94a3b8;margin-top:5px}
.mpe-toast{position:absolute;left:50%;bottom:22px;transform:translateX(-50%);background:#0f172a;color:#fff;font-size:12.5px;font-weight:700;padding:10px 18px;border-radius:99px;box-shadow:0 10px 25px -8px rgba(0,0,0,.4)}var form = document.getElementById('mpeForm');
var saveBtn = document.getElementById('mpeSave');
var cancelBtn = document.getElementById('mpeCancel');
var toast = document.getElementById('mpeToast');
var nameInput = document.getElementById('mpeName');
var emailInput = document.getElementById('mpeEmail');
var usernameInput = document.getElementById('mpeUsername');
var bioInput = document.getElementById('mpeBio');
var bioCount = document.getElementById('mpeBioCount');
var userHint = document.getElementById('mpeUserHint');
var initialValues = {};
Array.prototype.slice.call(form.querySelectorAll('input, textarea')).forEach(function (el) {
initialValues[el.id] = el.value;
});
var takenUsernames = ['jordan', 'admin', 'support', 'test'];
function checkDirty() {
var dirty = Array.prototype.slice.call(form.querySelectorAll('input, textarea')).some(function (el) {
return el.value !== initialValues[el.id];
});
saveBtn.disabled = !dirty;
}
form.addEventListener('input', function (e) {
checkDirty();
if (e.target === bioInput) {
bioCount.textContent = bioInput.value.length;
}
if (e.target === usernameInput) {
var val = usernameInput.value.trim().toLowerCase();
if (takenUsernames.indexOf(val) > -1) {
userHint.textContent = 'Username already taken';
userHint.className = 'mpe-hint taken';
} else {
userHint.textContent = 'Available';
userHint.className = 'mpe-hint';
}
}
});
document.getElementById('mpeFile').addEventListener('change', function (e) {
var file = e.target.files && e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function (ev) {
var avatar = document.getElementById('mpeAvatar');
avatar.style.backgroundImage = 'url(' + ev.target.result + ')';
avatar.style.backgroundSize = 'cover';
avatar.style.backgroundPosition = 'center';
avatar.textContent = '';
checkDirty();
};
reader.readAsDataURL(file);
});
function validate() {
var valid = true;
var nameErr = document.getElementById('mpeNameErr');
var emailErr = document.getElementById('mpeEmailErr');
if (!nameInput.value.trim()) {
nameInput.classList.add('invalid');
nameErr.hidden = false;
valid = false;
} else {
nameInput.classList.remove('invalid');
nameErr.hidden = true;
}
var emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailInput.value.trim());
if (!emailOk) {
emailInput.classList.add('invalid');
emailErr.hidden = false;
valid = false;
} else {
emailInput.classList.remove('invalid');
emailErr.hidden = true;
}
return valid;
}
saveBtn.addEventListener('click', function (e) {
e.preventDefault();
if (!validate()) return;
Array.prototype.slice.call(form.querySelectorAll('input, textarea')).forEach(function (el) {
initialValues[el.id] = el.value;
});
saveBtn.disabled = true;
toast.hidden = false;
setTimeout(function () { toast.hidden = true; }, 1800);
});
cancelBtn.addEventListener('click', function () {
Array.prototype.slice.call(form.querySelectorAll('input, textarea')).forEach(function (el) {
el.value = initialValues[el.id];
});
bioCount.textContent = bioInput.value.length;
userHint.textContent = 'Available';
userHint.className = 'mpe-hint';
checkDirty();
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Mobile Edit Profile Screen — Dirty-State Save, Avatar Upload & Live Validation

Editing a profile is different from viewing one — see the mobile profile screen for the read-only counterpart — because the interface needs to track what changed, prevent an accidental save of untouched data, and give feedback while the user is still typing rather than only after they submit. This snippet builds a complete edit-profile form: a real avatar upload with an instant image preview, a Save button that only enables once something has actually changed, a live bio character counter, a username-availability check as you type, and field-level validation errors that don't require a submit attempt to appear.
Dirty-state tracking, not just "form has values"
On load, initialValues snapshots every input and textarea's starting value by element id. Every subsequent input event calls checkDirty(), which compares each field's current value against its snapshot and enables Save only if at least one field genuinely differs from where it started. This is meaningfully different from just checking whether required fields are non-empty — a user who opens the form, types something, and then deletes it back to the original value should not have an enabled Save button implying there's something new to persist, and this implementation gets that right because it compares against the actual starting values, not just presence.
A real avatar upload with FileReader, not a stub
Clicking the small camera badge opens the native file picker (via a <label for="mpeFile"> wrapping a hidden <input type="file">, so no custom click-forwarding JavaScript is needed). Selecting an image reads it with FileReader.readAsDataURL(), and on load sets the avatar's background-image directly from the resulting data URL — a genuine local preview with no upload round-trip required to see the result, exactly like production avatar-picker flows show an immediate preview before the file finishes uploading in the background.
Validation that appears without a submit attempt
Name and email fields validate on every keystroke via the shared input listener rather than only when Save is clicked — an empty name or a malformed email shows its red border and inline error message immediately, then clears the moment the field becomes valid again. validate() is still called again inside the Save handler as the authoritative gate (so a user can't defeat validation by never triggering the live check), but by the time they reach Save, they've typically already seen and fixed any problems.
A username check with instant feedback
The username field checks its trimmed, lowercased value against a small takenUsernames array on every keystroke, flipping a hint line between "Available" (green) and "Username already taken" (red) instantly. In production, replace the local array lookup with a debounced request to a real availability-check endpoint, since checking uniqueness against a live username database cannot be done client-side — but keep the same instant-hint UI pattern once the network response resolves.
Cancel reverts, Save commits
Cancel doesn't just close the screen — it writes every field back to its initialValues snapshot, resets the bio counter and username hint, and re-runs checkDirty(), so backing out of an edit-in-progress cleanly discards unsaved changes without navigating away. Save, once validation passes, re-snapshots initialValues to the newly saved values (so Save becomes disabled again until something new changes) and shows a brief toast confirmation.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA profile edit form renders inside a phone frame with Save disabled since nothing has changed yet.
- 2Edit a fieldSave enables the moment any field genuinely differs from its starting value.
- 3Upload an avatarTap the camera badge to pick an image — it previews instantly in the circular avatar via FileReader.
- 4Watch live validationClear the name field or type an invalid email to see inline errors appear before you try to save.
- 5Check username availabilityType "admin" or "test" into the username field to see the taken-username hint switch to red.
- 6Save or CancelSave validates and shows a confirmation toast; Cancel reverts every field back to its original value.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
On load, every input and textarea\u2019s starting value is snapshotted into an initialValues object keyed by element id. On every input event, checkDirty() compares each field\u2019s current value against that snapshot and enables Save only if at least one field genuinely differs — not merely whether required fields are filled in.
No — clicking the camera badge opens the native file picker via a label wrapping a hidden file input, and the selected image is read locally with FileReader.readAsDataURL() to produce an instant preview as the avatar\u2019s background image. No network request is made; a production implementation would additionally upload the file to storage and persist the resulting URL on save.
Live validation gives the user a chance to see and correct a problem (like an empty name or malformed email) while they are still filling out the form, rather than only discovering it after clicking Save. The same validate() function still runs again inside the Save handler as the authoritative check, so validation cannot be bypassed even if the live listener were somehow skipped.
On every keystroke in the username field, the trimmed and lowercased value is checked against a small hardcoded takenUsernames array, and a hint line switches between green "Available" and red "Username already taken" accordingly. In production this local array must be replaced with a debounced request to a real backend availability-check endpoint, since uniqueness cannot be verified without querying the actual user database.
It writes every field\u2019s value back to its initialValues snapshot, resets the derived bio counter and username hint to match, and re-runs checkDirty() (which disables Save again since nothing now differs from the snapshot) — fully discarding any in-progress edits without navigating away from the screen.
Keep the form values in a single state object and a separate initialValues object captured on mount; derive "dirty" as a comparison between the two rather than a separate boolean flag that could drift out of sync. Handle the file input\u2019s change event the same way, reading the selected file with FileReader and storing the resulting data URL in state to drive the avatar\u2019s preview.