Mood Picker — Emoji Face Satisfaction Selector
Mood Picker · Forms · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<form class="mp-card" id="mpForm">
<h3>How was your experience?</h3>
<div class="mp-faces" id="mpFaces" role="radiogroup" aria-label="Mood">
<button type="button" class="mp-face" data-value="1" data-label="Awful" role="radio" aria-checked="false" aria-label="Awful">
<svg viewBox="0 0 48 48"><circle class="mp-bg" cx="24" cy="24" r="21"/><circle cx="17" cy="20" r="2.4"/><circle cx="31" cy="20" r="2.4"/><path class="mp-mouth" d="M16 33 Q24 26 32 33"/></svg>
</button>
<button type="button" class="mp-face" data-value="2" data-label="Bad" role="radio" aria-checked="false" aria-label="Bad">
<svg viewBox="0 0 48 48"><circle class="mp-bg" cx="24" cy="24" r="21"/><circle cx="17" cy="20" r="2.4"/><circle cx="31" cy="20" r="2.4"/><path class="mp-mouth" d="M16 31 Q24 28 32 31"/></svg>
</button>
<button type="button" class="mp-face" data-value="3" data-label="Okay" role="radio" aria-checked="false" aria-label="Okay">
<svg viewBox="0 0 48 48"><circle class="mp-bg" cx="24" cy="24" r="21"/><circle cx="17" cy="20" r="2.4"/><circle cx="31" cy="20" r="2.4"/><path class="mp-mouth" d="M16 30 L32 30"/></svg>
</button>
<button type="button" class="mp-face" data-value="4" data-label="Good" role="radio" aria-checked="false" aria-label="Good">
<svg viewBox="0 0 48 48"><circle class="mp-bg" cx="24" cy="24" r="21"/><circle cx="17" cy="20" r="2.4"/><circle cx="31" cy="20" r="2.4"/><path class="mp-mouth" d="M16 29 Q24 35 32 29"/></svg>
</button>
<button type="button" class="mp-face" data-value="5" data-label="Great" role="radio" aria-checked="false" aria-label="Great">
<svg viewBox="0 0 48 48"><circle class="mp-bg" cx="24" cy="24" r="21"/><circle cx="17" cy="20" r="2.4"/><circle cx="31" cy="20" r="2.4"/><path class="mp-mouth" d="M15 27 Q24 38 33 27"/></svg>
</button>
</div>
<p class="mp-label" id="mpLabel">Pick the face that fits</p>
<button class="mp-submit" type="submit" id="mpSubmit" disabled>Send feedback</button>
</form>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;display:flex;justify-content:center;padding:40px 20px}
.mp-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:26px;width:100%;max-width:380px;text-align:center;box-shadow:0 12px 32px -20px rgba(0,0,0,.3)}
.mp-card h3{font-size:16px;font-weight:800;color:#0f172a;margin-bottom:18px}
.mp-faces{display:flex;justify-content:space-between;gap:6px}
.mp-face{background:none;border:none;cursor:pointer;padding:2px;border-radius:50%;width:54px;height:54px;transition:transform .15s}
.mp-face svg{width:100%;height:100%;display:block}
.mp-face .mp-bg{fill:#e2e8f0;transition:fill .15s}
.mp-face svg circle:not(.mp-bg){fill:#64748b}
.mp-face .mp-mouth{fill:none;stroke:#64748b;stroke-width:2.4;stroke-linecap:round;transition:stroke .15s}
.mp-face:hover{transform:translateY(-3px) scale(1.06)}
.mp-face.mp-on{transform:translateY(-3px) scale(1.12)}
.mp-face.mp-on .mp-bg{fill:#fde68a}
.mp-face.mp-on svg circle:not(.mp-bg),.mp-face.mp-on .mp-mouth{fill:#92400e;stroke:#92400e}
.mp-face:focus-visible{outline:2px solid #6366f1;outline-offset:2px}
.mp-label{margin:16px 0 18px;font-size:14px;font-weight:700;color:#475569;min-height:20px}
.mp-submit{width:100%;background:#0f172a;color:#fff;border:none;border-radius:10px;padding:11px;font-size:13.5px;font-weight:700;cursor:pointer;font-family:inherit;transition:opacity .15s}
.mp-submit:disabled{opacity:.4;cursor:not-allowed}
.mp-submit:not(:disabled):hover{opacity:.9}var faces = Array.prototype.slice.call(document.querySelectorAll('.mp-face'));
var label = document.getElementById('mpLabel');
var submit = document.getElementById('mpSubmit');
var current = 0;
function paint(value) {
faces.forEach(function (f) {
var v = parseInt(f.getAttribute('data-value'), 10);
f.classList.toggle('mp-on', v === value);
f.setAttribute('aria-checked', String(v === current));
});
}
function preview(face) { paint(parseInt(face.getAttribute('data-value'), 10)); label.textContent = face.getAttribute('data-label'); }
function commit(face) {
current = parseInt(face.getAttribute('data-value'), 10);
submit.disabled = false;
paint(current);
label.textContent = face.getAttribute('data-label');
}
faces.forEach(function (face, i) {
face.addEventListener('mouseenter', function () { preview(face); });
face.addEventListener('focus', function () { preview(face); });
face.addEventListener('click', function () { commit(face); });
face.addEventListener('keydown', function (e) {
if (e.key === 'ArrowRight') { e.preventDefault(); var n = faces[Math.min(i + 1, faces.length - 1)]; n.focus(); commit(n); }
if (e.key === 'ArrowLeft') { e.preventDefault(); var p = faces[Math.max(i - 1, 0)]; p.focus(); commit(p); }
});
});
document.getElementById('mpFaces').addEventListener('mouseleave', function () {
if (current) { paint(current); label.textContent = faces[current - 1].getAttribute('data-label'); }
else { paint(0); label.textContent = 'Pick the face that fits'; }
});
document.getElementById('mpForm').addEventListener('submit', function (e) {
e.preventDefault();
label.textContent = 'Thanks for the feedback!';
});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
Mood Picker — SVG Smiley-Face Satisfaction Rating with Hover Preview

A mood picker asks "how was it?" with a row of faces from frowning to grinning — a friendlier, more immediate rating than stars for satisfaction surveys, support closeouts, and check-ins. This snippet builds one with crisp inline-SVG faces, a hover preview, click-to-commit, and keyboard support, in plain HTML, CSS, and vanilla JavaScript with no emoji fonts or images.
Faces drawn in SVG, not emoji
Each face is an inline <svg> with two eyes and a single <path> mouth. The mouth's curve encodes the mood: a downward Q quadratic curve for sad, a flat L line for neutral, and an upward curve for happy. Drawing them in SVG instead of using emoji characters means they render identically on every OS (no "emoji looks different on Windows vs Mac" problem), scale crisply at any size, and recolour with CSS — the selected face turns warm amber via a class, with no extra assets.
Preview vs commit
Like a good rating control, hovering or focusing a face previews that mood — lifting it, colouring it, and showing its label ("Okay", "Great") — without changing the saved value. Clicking commits: it stores the score, enables the submit button, and becomes the state the row returns to when the pointer leaves. This separation lets users scan the options without accidentally locking in a choice.
Accessible by construction
The row is a role="radiogroup" and each face is a <button role="radio"> with an aria-label ("Good") and aria-checked tracking the committed value. Arrow keys move between faces and set the mood, matching the radio-group keyboard model, and a focus ring keeps keyboard users oriented. Screen-reader users hear a clear label for each option rather than an ambiguous picture.
Form-ready feedback loop
The submit button stays disabled until a face is chosen, preventing empty responses, and the label doubles as live confirmation ("Thanks for the feedback!"). The committed value is a number 1–5 you can post to your backend or map to an NPS-style bucket. Because the logic is tiny and the faces are pure markup, you can add a comment box, change the count to three faces, or swap the palette in minutes.
Tactile motion
Faces lift and scale on hover with a short transition, giving the control a playful, responsive feel that suits the friendly tone of a mood survey. Everything is a few dozen lines of CSS — a clean, dependency-free reference for emoji-style satisfaction pickers.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA row of five SVG faces renders with a disabled submit button.
- 2Hover a faceIt lifts and the label shows its mood, without committing.
- 3Click to chooseClicking sets the mood, enables submit, and highlights the face.
- 4Use the keyboardTab to the row and use Left/Right arrows to move and select.
- 5Read the valueThe committed value is 1–5 (data-value); post or map it as you like.
- 6CustomiseChange to three faces, recolour, or add a comment field.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Emoji render differently on every platform — the same character looks distinct on Windows, macOS, Android, and iOS, so your survey loses visual consistency. Inline SVG faces look identical everywhere, scale crisply to any size, and recolour with CSS (the selected face turns amber), all without bundling image files. You also get full control over the exact expression via the mouth path.
Each face has a data-value from 1 (awful) to 5 (great), and the committed choice is held in a current variable. Read it on submit to post a number to your backend, or map it to buckets — for example treat 4 and 5 as promoters. The control also exposes a readable label per face for confirmations.
Yes. The row is a radiogroup, each face is a button with role="radio", an aria-label, and aria-checked, and arrow keys move and set the mood like a native radio group. A focus-visible ring shows the active face, and screen readers announce a clear word ("Good") instead of an image, so it is usable without sight or a mouse.
Yes — remove two of the button blocks and keep data-value sequential (1, 2, 3). The script reads whatever faces are present and the arrow-key navigation adapts automatically, so no JavaScript changes are needed. Three faces (sad/neutral/happy) is common for lightweight one-tap sentiment.
Keep the SVG markup and move the state into your component: track committed and hovered values, render the highlight class from them, and bind mouseenter/focus/click/keydown handlers. Replace the submit gating with a disabled prop driven by state, and post the numeric value in your submit handler. Tailwind users swap the classes for utilities and toggle the amber fill with a conditional class.