Mood
import { useState } from 'react';
const css = `
@keyframes field-mood-blink { 0%, 94%, 100% { transform: scaleY(1); } 97% { transform: scaleY(0.1); } }
@keyframes field-mood-word { from { opacity: 0; transform: translateY(6px); } }
.field-mood { display: grid; justify-items: center; width: 230px; }
.field-mood__face { width: 76px; height: 76px; margin-bottom: 4px; }
.field-mood__head { fill: light-dark(#ebedf0, #232529); stroke: light-dark(#cbced3, #393c41); stroke-width: 1.5; }
.field-mood__eye { fill: light-dark(#373a40, #f1f2f3); transform-box: fill-box; transform-origin: center; animation: field-mood-blink 4.5s infinite; }
.field-mood__brow, .field-mood__mouth { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 3; stroke-linecap: round; }
.field-mood__cheek { fill: light-dark(#6d7179, #7d8087); }
.field-mood__word { height: 22px; color: light-dark(#373a40, #f1f2f3); font-size: 15px; font-weight: 600; animation: field-mood-word 0.25s ease-out; }
.field-mood__label { margin-bottom: 10px; color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.field-mood__range { width: 100%; height: 22px; margin: 0; background: none; -webkit-appearance: none; appearance: none; cursor: pointer; }
.field-mood__range::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, light-dark(#dbdde1, #2e3035), light-dark(#50535b, #c6c8cd)); }
.field-mood__range::-moz-range-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, light-dark(#dbdde1, #2e3035), light-dark(#50535b, #c6c8cd)); }
.field-mood__range::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; margin-top: -8px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 2px 6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-mood__range::-moz-range-thumb { width: 22px; height: 22px; border: 0; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 2px 6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-mood__range:focus-visible { outline: none; }
.field-mood__range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 0 0 6px light-dark(#5f626a, #a7abb2); }
.field-mood__range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 0 0 6px light-dark(#5f626a, #a7abb2); }
@media (prefers-reduced-motion: reduce) { .field-mood__eye, .field-mood__word { animation: none; } }
`;
const WORDS = ['Awful', 'Bad', 'Okay', 'Good', 'Great'];
export function MoodInput() {
const [v, setV] = useState(70);
const t = (v - 50) / 50; // -1 sad … 1 happy
const word = WORDS[Math.min(4, Math.floor(v / 20))];
// The mouth bends, the brows tilt and the eyes open wider as the mood lifts.
const mouth = `M26 ${52 - t * 2} Q38 ${52 + t * 11} 50 ${52 - t * 2}`;
const brow = (x: number, dir: number) => `M${x - 6} ${24 - t * dir * 3} L${x + 6} ${24 + t * dir * 3}`;
const eyeRy = 3.4 + Math.max(0, t) * 1.2;
return (
<>
<style>
{css}
</style>
<div className="field-mood">
<svg className="field-mood__face" viewBox="0 0 76 76" aria-hidden="true">
<circle className="field-mood__head" cx="38" cy="38" r="35" />
<path className="field-mood__brow" d={brow(27, -1)} />
<path className="field-mood__brow" d={brow(49, 1)} />
<ellipse className="field-mood__eye" cx="27" cy="34" rx="3.4" ry={eyeRy} />
<ellipse className="field-mood__eye" cx="49" cy="34" rx="3.4" ry={eyeRy} style={{ animationDelay: '0.05s' }} />
<circle className="field-mood__cheek" cx="19" cy="45" r="3.5" opacity={Math.max(0, t) * 0.8} />
<circle className="field-mood__cheek" cx="57" cy="45" r="3.5" opacity={Math.max(0, t) * 0.8} />
<path className="field-mood__mouth" d={mouth} />
</svg>
<span key={word} className="field-mood__word" aria-hidden="true">
{word}
</span>
<label className="field-mood__label" htmlFor="field-mood-range">
How was your session?
</label>
<input id="field-mood-range" className="field-mood__range" type="range" min={0} max={100} value={v} aria-valuetext={word} onChange={(e) => setV(Number(e.target.value))} />
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldMood(el) on the mounted element instead.
<div class="field-mood">
<svg class="field-mood__face" viewBox="0 0 76 76" aria-hidden="true">
<circle class="field-mood__head" cx="38" cy="38" r="35"></circle>
<path class="field-mood__brow" d="M21 25.2 L33 22.8"></path>
<path class="field-mood__brow" d="M43 22.8 L55 25.2"></path>
<ellipse class="field-mood__eye" cx="27" cy="34" rx="3.4" ry="3.88"></ellipse>
<ellipse class="field-mood__eye" cx="49" cy="34" rx="3.4" ry="3.88" style="animation-delay:0.05s"></ellipse>
<circle class="field-mood__cheek" cx="19" cy="45" r="3.5" opacity="0.32000000000000006"></circle>
<circle class="field-mood__cheek" cx="57" cy="45" r="3.5" opacity="0.32000000000000006"></circle>
<path class="field-mood__mouth" d="M26 51.2 Q38 56.4 50 51.2"></path>
</svg>
<span class="field-mood__word" aria-hidden="true">Good</span>
<label class="field-mood__label" for="field-mood-range">How was your session?</label>
<input id="field-mood-range" class="field-mood__range" type="range" min="0" max="100" aria-valuetext="Good" value="70"/>
</div>@keyframes field-mood-blink { 0%, 94%, 100% { transform: scaleY(1); } 97% { transform: scaleY(0.1); } }
@keyframes field-mood-word { from { opacity: 0; transform: translateY(6px); } }
.field-mood { display: grid; justify-items: center; width: 230px; }
.field-mood__face { width: 76px; height: 76px; margin-bottom: 4px; }
.field-mood__head { fill: light-dark(#ebedf0, #232529); stroke: light-dark(#cbced3, #393c41); stroke-width: 1.5; }
.field-mood__eye { fill: light-dark(#373a40, #f1f2f3); transform-box: fill-box; transform-origin: center; animation: field-mood-blink 4.5s infinite; }
.field-mood__brow, .field-mood__mouth { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 3; stroke-linecap: round; }
.field-mood__cheek { fill: light-dark(#6d7179, #7d8087); }
.field-mood__word { height: 22px; color: light-dark(#373a40, #f1f2f3); font-size: 15px; font-weight: 600; animation: field-mood-word 0.25s ease-out; }
.field-mood__label { margin-bottom: 10px; color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.field-mood__range { width: 100%; height: 22px; margin: 0; background: none; -webkit-appearance: none; appearance: none; cursor: pointer; }
.field-mood__range::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, light-dark(#dbdde1, #2e3035), light-dark(#50535b, #c6c8cd)); }
.field-mood__range::-moz-range-track { height: 6px; border-radius: 3px; background: linear-gradient(90deg, light-dark(#dbdde1, #2e3035), light-dark(#50535b, #c6c8cd)); }
.field-mood__range::-webkit-slider-thumb { -webkit-appearance: none; width: 22px; height: 22px; margin-top: -8px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 2px 6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-mood__range::-moz-range-thumb { width: 22px; height: 22px; border: 0; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 2px 6px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
.field-mood__range:focus-visible { outline: none; }
.field-mood__range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 0 0 6px light-dark(#5f626a, #a7abb2); }
.field-mood__range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 4px light-dark(#f3f4f7, #1a1c1f), 0 0 0 6px light-dark(#5f626a, #a7abb2); }
@media (prefers-reduced-motion: reduce) { .field-mood__eye, .field-mood__word { animation: none; } }function initFieldMood(root) {
var range = root.querySelector('.field-mood__range');
var word = root.querySelector('.field-mood__word');
var mouth = root.querySelector('.field-mood__mouth');
var brows = root.querySelectorAll('.field-mood__brow');
var eyes = root.querySelectorAll('.field-mood__eye');
var cheeks = root.querySelectorAll('.field-mood__cheek');
if (!range || !word || !mouth || brows.length !== 2) return;
var WORDS = ['Awful', 'Bad', 'Okay', 'Good', 'Great'];
function brow(x, dir, t) { return 'M' + (x - 6) + ' ' + (24 - t * dir * 3) + ' L' + (x + 6) + ' ' + (24 + t * dir * 3); }
function update() {
var v = Number(range.value);
var t = (v - 50) / 50; // -1 sad, 1 happy
var w = WORDS[Math.min(4, Math.floor(v / 20))];
// The mouth bends, the brows tilt and the eyes open wider as the mood lifts.
mouth.setAttribute('d', 'M26 ' + (52 - t * 2) + ' Q38 ' + (52 + t * 11) + ' 50 ' + (52 - t * 2));
brows[0].setAttribute('d', brow(27, -1, t));
brows[1].setAttribute('d', brow(49, 1, t));
eyes.forEach(function (e) { e.setAttribute('ry', String(3.4 + Math.max(0, t) * 1.2)); });
cheeks.forEach(function (c) { c.setAttribute('opacity', String(Math.max(0, t) * 0.8)); });
range.setAttribute('aria-valuetext', w);
if (word.textContent !== w) {
// The word flips in when it changes.
var fresh = word.cloneNode(false);
fresh.textContent = w;
word.replaceWith(fresh);
word = fresh;
}
}
range.addEventListener('input', update);
return function () { range.removeEventListener('input', update); };
}
document.querySelectorAll('.field-mood').forEach(initFieldMood);Goal: Create a "Mood" input.
Visual behavior: A rating slider with a face: drag it and the mouth bends from a frown to a grin, the brows tilt, the eyes open wider and the cheeks blush as the mood rises, with the word flipping above. The face blinks now and then.
Technique:
- A native range input (0 to 100) drives t = (v - 50) / 50, from -1 sad to 1 happy.
- The mouth is a quadratic SVG path whose control point drops by t x 11; the brows are lines whose ends move by t; eye height and cheek opacity grow on the happy side.
- The word for the current fifth of the range flips in when it changes; the eyes blink on a long keyframe that is still most of the time.
Accessibility: A native range input with a <label> and an aria-valuetext word, so keyboard and screen readers work as normal. Under prefers-reduced-motion it does not blink.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Mood" input.
Visual behavior: A rating slider with a face: drag it and the mouth bends from a frown to a grin, the brows tilt, the eyes open wider and the cheeks blush as the mood rises, with the word flipping above. The face blinks now and then.
Technique:
- A native range input (0 to 100) drives t = (v - 50) / 50, from -1 sad to 1 happy.
- The mouth is a quadratic SVG path whose control point drops by t x 11; the brows are lines whose ends move by t; eye height and cheek opacity grow on the happy side.
- The word for the current fifth of the range flips in when it changes; the eyes blink on a long keyframe that is still most of the time.
Accessibility: A native range input with a <label> and an aria-valuetext word, so keyboard and screen readers work as normal. Under prefers-reduced-motion it does not blink.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A rating slider with a face: drag it and the mouth bends from a frown to a grin, the brows tilt, the eyes open wider and the cheeks blush as the mood rises, with the word flipping above. The face blinks now and then.
- Dependencies
- None
- Difficulty
- Beginner
- Triggers
- click, morph, state change
- Techniques
- svg, keyframes, css transition
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input with a <label> and an aria-valuetext word, so keyboard and screen readers work as normal. Under prefers-reduced-motion it does not blink.