Egg
import { useState, type CSSProperties } from 'react';
const MIN = 3;
const MAX = 12;
const word = (m: number) => (m <= 3 ? 'Runny' : m <= 5 ? 'Soft' : m <= 7 ? 'Jammy' : m <= 9 ? 'Medium' : 'Hard');
// The yolk sets from the outside in: a glossy runny centre that shrinks, a
// paler set ring that grows, a drip that only a runny yolk makes, and the
// grey line round an overdone one.
const css = `
.slider-egg { --white: light-dark(#fbfcfd, #e5e6e9); --shell: light-dark(#ebedf0, #c6c8cd); --cup: light-dark(#6d7179, #393c41); --runny: light-dark(#50535b, #7d8087); --set: light-dark(#cbced3, #a7abb2); display: grid; gap: 12px; width: 230px; }
.slider-egg__stand { position: relative; height: 122px; }
/* The egg in its cup, the top sliced off and lying beside it. */
.slider-egg__shell { position: absolute; left: 83px; top: 4px; width: 64px; height: 96px; clip-path: inset(24px 0 0 0); border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; background: radial-gradient(ellipse at 34% 40%, var(--shell), color-mix(in oklab, var(--shell) 82%, light-dark(#6d7179, #7d8087))); }
.slider-egg__cut { position: absolute; left: 89px; top: 21.5px; width: 52px; height: 13px; border-radius: 50%; background: var(--white); box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--shell) 70%, light-dark(#6d7179, #7d8087)), inset 0 2px 3px color-mix(in oklab, light-dark(#6d7179, #7d8087) 30%, transparent); }
.slider-egg__yolk { position: absolute; left: 50%; top: 50%; width: 32px; height: 8px; translate: -50% -50%; scale: calc(1 - var(--slider-egg-v) * 0.1); border-radius: 50%; background: radial-gradient(ellipse, var(--runny) 0 calc((1 - var(--slider-egg-v)) * 64%), var(--set) calc((1 - var(--slider-egg-v)) * 64% + 12%)); box-shadow: 0 0 0 1.5px color-mix(in oklab, light-dark(#6d7179, #7d8087) calc(clamp(0, (var(--slider-egg-v) - 0.78) * 5, 1) * 80%), transparent); transition: scale 0.3s; }
/* Gloss on a runny yolk, crumbs on a hard one. */
.slider-egg__yolk::before { content: ''; position: absolute; left: 20%; top: 18%; width: 28%; height: 34%; border-radius: 50%; background: color-mix(in oklab, var(--white) 75%, transparent); opacity: clamp(0, calc(1 - var(--slider-egg-v) * 1.6), 1); }
.slider-egg__yolk::after { content: ''; position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle, color-mix(in oklab, var(--white) 50%, transparent) 0.7px, transparent 1.1px) 0 0 / 4px 3px, radial-gradient(circle, color-mix(in oklab, light-dark(#6d7179, #7d8087) 60%, transparent) 0.7px, transparent 1.1px) 2px 1px / 5px 4px; opacity: clamp(0, calc((var(--slider-egg-v) - 0.55) * 2.4), 1); }
/* Only a runny yolk spills over the rim and runs down the shell. */
.slider-egg__drip { position: absolute; left: 134px; top: 29px; width: 7px; height: calc(8px + (1 - var(--slider-egg-v)) * 22px); border-radius: 4px 4px 5px 5px; rotate: -12deg; transform-origin: 50% 0; background: var(--runny); opacity: clamp(0, calc((0.3 - var(--slider-egg-v)) * 6), 1); transition: height 0.35s ease-out, opacity 0.25s; }
.slider-egg__drip::after { content: ''; position: absolute; left: 50%; bottom: -3px; width: 9px; height: 9px; translate: -50% 0; border-radius: 50%; background: var(--runny); }
.slider-egg__cup { position: absolute; left: 79px; top: 70px; width: 72px; height: 34px; border-radius: 4px 4px 50% 50% / 4px 4px 100% 100%; background: linear-gradient(90deg, color-mix(in oklab, var(--cup) 75%, light-dark(#fbfcfd, #121417)), var(--cup) 35%, color-mix(in oklab, var(--cup) 70%, light-dark(#373a40, #f1f2f3)) 52%, color-mix(in oklab, var(--cup) 80%, light-dark(#fbfcfd, #121417))); box-shadow: inset 0 3px 0 color-mix(in oklab, var(--cup) 60%, light-dark(#373a40, #f1f2f3)); }
.slider-egg__stem { position: absolute; left: 107px; top: 102px; width: 16px; height: 10px; background: linear-gradient(90deg, color-mix(in oklab, var(--cup) 75%, light-dark(#fbfcfd, #121417)), var(--cup), color-mix(in oklab, var(--cup) 80%, light-dark(#fbfcfd, #121417))); }
.slider-egg__foot { position: absolute; left: 89px; top: 110px; width: 56px; height: 11px; border-radius: 50%; background: linear-gradient(90deg, color-mix(in oklab, var(--cup) 75%, light-dark(#fbfcfd, #121417)), var(--cup) 40%, color-mix(in oklab, var(--cup) 80%, light-dark(#fbfcfd, #121417))); box-shadow: 0 6px 10px -6px light-dark(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6)); }
.slider-egg__cap { position: absolute; left: 166px; top: 98px; width: 34px; height: 22px; rotate: 18deg; border-radius: 50% 50% 0 0 / 100% 100% 0 0; background: radial-gradient(ellipse at 40% 70%, var(--shell), color-mix(in oklab, var(--shell) 80%, light-dark(#6d7179, #7d8087))); }
.slider-egg__cap::after { content: ''; position: absolute; left: 3px; right: 3px; bottom: -4px; height: 8px; border-radius: 50%; background: var(--white); box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--shell) 70%, light-dark(#6d7179, #7d8087)); }
.slider-egg__steam { position: absolute; left: 0; top: 0; width: 230px; height: 40px; fill: none; stroke: color-mix(in oklab, light-dark(#50535b, #c6c8cd) 60%, transparent); stroke-width: 2; stroke-linecap: round; }
.slider-egg__steam path { animation: slider-egg-steam 2.6s ease-in-out infinite; }
@keyframes slider-egg-steam { 0% { transform: translateY(10px); opacity: 0; } 40% { opacity: 1; } 100% { transform: translateY(-8px); opacity: 0; } }
.slider-egg__read { display: flex; align-items: baseline; justify-content: space-between; }
.slider-egg__min { color: light-dark(#373a40, #f1f2f3); font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-egg__min b { font-weight: inherit; }
.slider-egg__min small { margin-left: 4px; color: light-dark(#5f626a, #a7abb2); font-size: 11px; font-weight: 500; }
.slider-egg__word { padding: 2px 9px; border-radius: 999px; background: light-dark(#ebedf0, #232529); color: light-dark(#42454c, #e5e6e9); font-size: 11px; font-weight: 600; }
.slider-egg__track { position: relative; height: 30px; }
.slider-egg__rail { position: absolute; left: 0; right: 0; top: 10px; height: 4px; border-radius: 2px; background: linear-gradient(90deg, var(--runny), var(--set)); opacity: 0.7; }
.slider-egg__tick { position: absolute; top: 18px; width: 1px; height: 4px; background: light-dark(#cbced3, #393c41); }
.slider-egg__tick b { position: absolute; left: 50%; top: 5px; translate: -50% 0; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; }
/* The handle is an egg. */
.slider-egg__thumb { position: absolute; top: 0; left: calc(var(--slider-egg-v) * (100% - 18px)); width: 18px; height: 23px; border-radius: 50% 50% 50% 50% / 58% 58% 42% 42%; background: radial-gradient(ellipse at 38% 30%, var(--white), color-mix(in oklab, var(--white) 82%, light-dark(#6d7179, #7d8087))); box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.5)), inset 0 0 0 1px color-mix(in oklab, var(--white) 70%, light-dark(#6d7179, #7d8087)); }
.slider-egg:has(.slider-egg__input:focus-visible) .slider-egg__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-egg__input { position: absolute; inset: 0 0 8px; width: 100%; height: 24px; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-egg__input::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 24px; }
.slider-egg__input::-moz-range-thumb { width: 18px; height: 24px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-egg * { animation: none !important; transition: none !important; }
}
`;
export function Preview() {
const [m, setM] = useState(6);
const w = word(m);
return (
<>
<style href="kit-sliders-egg" precedence="kit">
{css}
</style>
<div className="slider-egg" style={{ '--slider-egg-v': ((m - MIN) / (MAX - MIN)).toFixed(4) } as CSSProperties}>
<div className="slider-egg__stand" aria-hidden="true">
<svg className="slider-egg__steam" viewBox="0 0 230 40">
<path d="M108 22 c-4 -5 4 -8 0 -12 c-4 -5 4 -8 0 -12" />
<path d="M122 22 c-4 -5 4 -8 0 -12 c-4 -5 4 -8 0 -12" style={{ animationDelay: '-1.3s' }} />
</svg>
<span className="slider-egg__shell" />
<span className="slider-egg__cut">
<span className="slider-egg__yolk" />
</span>
<span className="slider-egg__drip" />
<span className="slider-egg__cup" />
<span className="slider-egg__stem" />
<span className="slider-egg__foot" />
<span className="slider-egg__cap" />
</div>
<div className="slider-egg__read" aria-hidden="true">
<span className="slider-egg__min">
<b>{m}</b>
<small>min</small>
</span>
<span className="slider-egg__word">{w}</span>
</div>
<div className="slider-egg__track">
<span className="slider-egg__rail" aria-hidden="true" />
{Array.from({ length: MAX - MIN + 1 }, (_, i) => (
<span key={i} className="slider-egg__tick" style={{ left: `calc(9px + ${i / (MAX - MIN)} * (100% - 18px))` }} aria-hidden="true">
{(i + MIN) % 3 === 0 && <b>{i + MIN}</b>}
</span>
))}
<span className="slider-egg__thumb" aria-hidden="true" />
<input className="slider-egg__input" type="range" min={MIN} max={MAX} value={m} onChange={(e) => setM(+e.target.value)} aria-label="Boiling time" aria-valuetext={`${m} minutes, ${w.toLowerCase()}`} />
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initSliderEgg(el) on the mounted element instead.
<div class="slider-egg" style="--slider-egg-v:0.3333">
<div class="slider-egg__stand" aria-hidden="true">
<svg class="slider-egg__steam" viewBox="0 0 230 40">
<path d="M108 22 c-4 -5 4 -8 0 -12 c-4 -5 4 -8 0 -12"></path>
<path d="M122 22 c-4 -5 4 -8 0 -12 c-4 -5 4 -8 0 -12" style="animation-delay:-1.3s"></path>
</svg>
<span class="slider-egg__shell"></span>
<span class="slider-egg__cut">
<span class="slider-egg__yolk"></span>
</span>
<span class="slider-egg__drip"></span>
<span class="slider-egg__cup"></span>
<span class="slider-egg__stem"></span>
<span class="slider-egg__foot"></span>
<span class="slider-egg__cap"></span>
</div>
<div class="slider-egg__read" aria-hidden="true">
<span class="slider-egg__min">
<b>6</b>
<small>min</small>
</span>
<span class="slider-egg__word">Jammy</span>
</div>
<div class="slider-egg__track">
<span class="slider-egg__rail" aria-hidden="true"></span>
<span class="slider-egg__tick" style="left:calc(9px + 0 * (100% - 18px))" aria-hidden="true">
<b>3</b>
</span>
<span class="slider-egg__tick" style="left:calc(9px + 0.1111111111111111 * (100% - 18px))" aria-hidden="true"></span>
<span class="slider-egg__tick" style="left:calc(9px + 0.2222222222222222 * (100% - 18px))" aria-hidden="true"></span>
<span class="slider-egg__tick" style="left:calc(9px + 0.3333333333333333 * (100% - 18px))" aria-hidden="true">
<b>6</b>
</span>
<span class="slider-egg__tick" style="left:calc(9px + 0.4444444444444444 * (100% - 18px))" aria-hidden="true"></span>
<span class="slider-egg__tick" style="left:calc(9px + 0.5555555555555556 * (100% - 18px))" aria-hidden="true"></span>
<span class="slider-egg__tick" style="left:calc(9px + 0.6666666666666666 * (100% - 18px))" aria-hidden="true">
<b>9</b>
</span>
<span class="slider-egg__tick" style="left:calc(9px + 0.7777777777777778 * (100% - 18px))" aria-hidden="true"></span>
<span class="slider-egg__tick" style="left:calc(9px + 0.8888888888888888 * (100% - 18px))" aria-hidden="true"></span>
<span class="slider-egg__tick" style="left:calc(9px + 1 * (100% - 18px))" aria-hidden="true">
<b>12</b>
</span>
<span class="slider-egg__thumb" aria-hidden="true"></span>
<input class="slider-egg__input" type="range" min="3" max="12" aria-label="Boiling time" aria-valuetext="6 minutes, jammy" value="6"/>
</div>
</div>.slider-egg { --white: light-dark(#fbfcfd, #e5e6e9); --shell: light-dark(#ebedf0, #c6c8cd); --cup: light-dark(#6d7179, #393c41); --runny: light-dark(#50535b, #7d8087); --set: light-dark(#cbced3, #a7abb2); display: grid; gap: 12px; width: 230px; }
.slider-egg__stand { position: relative; height: 122px; }
/* The egg in its cup, the top sliced off and lying beside it. */
.slider-egg__shell { position: absolute; left: 83px; top: 4px; width: 64px; height: 96px; clip-path: inset(24px 0 0 0); border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%; background: radial-gradient(ellipse at 34% 40%, var(--shell), color-mix(in oklab, var(--shell) 82%, light-dark(#6d7179, #7d8087))); }
.slider-egg__cut { position: absolute; left: 89px; top: 21.5px; width: 52px; height: 13px; border-radius: 50%; background: var(--white); box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--shell) 70%, light-dark(#6d7179, #7d8087)), inset 0 2px 3px color-mix(in oklab, light-dark(#6d7179, #7d8087) 30%, transparent); }
.slider-egg__yolk { position: absolute; left: 50%; top: 50%; width: 32px; height: 8px; translate: -50% -50%; scale: calc(1 - var(--slider-egg-v) * 0.1); border-radius: 50%; background: radial-gradient(ellipse, var(--runny) 0 calc((1 - var(--slider-egg-v)) * 64%), var(--set) calc((1 - var(--slider-egg-v)) * 64% + 12%)); box-shadow: 0 0 0 1.5px color-mix(in oklab, light-dark(#6d7179, #7d8087) calc(clamp(0, (var(--slider-egg-v) - 0.78) * 5, 1) * 80%), transparent); transition: scale 0.3s; }
/* Gloss on a runny yolk, crumbs on a hard one. */
.slider-egg__yolk::before { content: ''; position: absolute; left: 20%; top: 18%; width: 28%; height: 34%; border-radius: 50%; background: color-mix(in oklab, var(--white) 75%, transparent); opacity: clamp(0, calc(1 - var(--slider-egg-v) * 1.6), 1); }
.slider-egg__yolk::after { content: ''; position: absolute; inset: 0; border-radius: 50%; background: radial-gradient(circle, color-mix(in oklab, var(--white) 50%, transparent) 0.7px, transparent 1.1px) 0 0 / 4px 3px, radial-gradient(circle, color-mix(in oklab, light-dark(#6d7179, #7d8087) 60%, transparent) 0.7px, transparent 1.1px) 2px 1px / 5px 4px; opacity: clamp(0, calc((var(--slider-egg-v) - 0.55) * 2.4), 1); }
/* Only a runny yolk spills over the rim and runs down the shell. */
.slider-egg__drip { position: absolute; left: 134px; top: 29px; width: 7px; height: calc(8px + (1 - var(--slider-egg-v)) * 22px); border-radius: 4px 4px 5px 5px; rotate: -12deg; transform-origin: 50% 0; background: var(--runny); opacity: clamp(0, calc((0.3 - var(--slider-egg-v)) * 6), 1); transition: height 0.35s ease-out, opacity 0.25s; }
.slider-egg__drip::after { content: ''; position: absolute; left: 50%; bottom: -3px; width: 9px; height: 9px; translate: -50% 0; border-radius: 50%; background: var(--runny); }
.slider-egg__cup { position: absolute; left: 79px; top: 70px; width: 72px; height: 34px; border-radius: 4px 4px 50% 50% / 4px 4px 100% 100%; background: linear-gradient(90deg, color-mix(in oklab, var(--cup) 75%, light-dark(#fbfcfd, #121417)), var(--cup) 35%, color-mix(in oklab, var(--cup) 70%, light-dark(#373a40, #f1f2f3)) 52%, color-mix(in oklab, var(--cup) 80%, light-dark(#fbfcfd, #121417))); box-shadow: inset 0 3px 0 color-mix(in oklab, var(--cup) 60%, light-dark(#373a40, #f1f2f3)); }
.slider-egg__stem { position: absolute; left: 107px; top: 102px; width: 16px; height: 10px; background: linear-gradient(90deg, color-mix(in oklab, var(--cup) 75%, light-dark(#fbfcfd, #121417)), var(--cup), color-mix(in oklab, var(--cup) 80%, light-dark(#fbfcfd, #121417))); }
.slider-egg__foot { position: absolute; left: 89px; top: 110px; width: 56px; height: 11px; border-radius: 50%; background: linear-gradient(90deg, color-mix(in oklab, var(--cup) 75%, light-dark(#fbfcfd, #121417)), var(--cup) 40%, color-mix(in oklab, var(--cup) 80%, light-dark(#fbfcfd, #121417))); box-shadow: 0 6px 10px -6px light-dark(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.6)); }
.slider-egg__cap { position: absolute; left: 166px; top: 98px; width: 34px; height: 22px; rotate: 18deg; border-radius: 50% 50% 0 0 / 100% 100% 0 0; background: radial-gradient(ellipse at 40% 70%, var(--shell), color-mix(in oklab, var(--shell) 80%, light-dark(#6d7179, #7d8087))); }
.slider-egg__cap::after { content: ''; position: absolute; left: 3px; right: 3px; bottom: -4px; height: 8px; border-radius: 50%; background: var(--white); box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--shell) 70%, light-dark(#6d7179, #7d8087)); }
.slider-egg__steam { position: absolute; left: 0; top: 0; width: 230px; height: 40px; fill: none; stroke: color-mix(in oklab, light-dark(#50535b, #c6c8cd) 60%, transparent); stroke-width: 2; stroke-linecap: round; }
.slider-egg__steam path { animation: slider-egg-steam 2.6s ease-in-out infinite; }
@keyframes slider-egg-steam { 0% { transform: translateY(10px); opacity: 0; } 40% { opacity: 1; } 100% { transform: translateY(-8px); opacity: 0; } }
.slider-egg__read { display: flex; align-items: baseline; justify-content: space-between; }
.slider-egg__min { color: light-dark(#373a40, #f1f2f3); font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.slider-egg__min b { font-weight: inherit; }
.slider-egg__min small { margin-left: 4px; color: light-dark(#5f626a, #a7abb2); font-size: 11px; font-weight: 500; }
.slider-egg__word { padding: 2px 9px; border-radius: 999px; background: light-dark(#ebedf0, #232529); color: light-dark(#42454c, #e5e6e9); font-size: 11px; font-weight: 600; }
.slider-egg__track { position: relative; height: 30px; }
.slider-egg__rail { position: absolute; left: 0; right: 0; top: 10px; height: 4px; border-radius: 2px; background: linear-gradient(90deg, var(--runny), var(--set)); opacity: 0.7; }
.slider-egg__tick { position: absolute; top: 18px; width: 1px; height: 4px; background: light-dark(#cbced3, #393c41); }
.slider-egg__tick b { position: absolute; left: 50%; top: 5px; translate: -50% 0; color: light-dark(#6d7179, #7d8087); font-size: 9px; font-weight: 600; }
/* The handle is an egg. */
.slider-egg__thumb { position: absolute; top: 0; left: calc(var(--slider-egg-v) * (100% - 18px)); width: 18px; height: 23px; border-radius: 50% 50% 50% 50% / 58% 58% 42% 42%; background: radial-gradient(ellipse at 38% 30%, var(--white), color-mix(in oklab, var(--white) 82%, light-dark(#6d7179, #7d8087))); box-shadow: 0 2px 6px light-dark(rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.5)), inset 0 0 0 1px color-mix(in oklab, var(--white) 70%, light-dark(#6d7179, #7d8087)); }
.slider-egg:has(.slider-egg__input:focus-visible) .slider-egg__thumb { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; }
.slider-egg__input { position: absolute; inset: 0 0 8px; width: 100%; height: 24px; margin: 0; opacity: 0; cursor: pointer; -webkit-appearance: none; appearance: none; background: none; }
.slider-egg__input::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 24px; }
.slider-egg__input::-moz-range-thumb { width: 18px; height: 24px; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slider-egg * { animation: none !important; transition: none !important; }
}function initSliderEgg(root) {
var input = root.querySelector('.slider-egg__input');
var number = root.querySelector('.slider-egg__min b');
var label = root.querySelector('.slider-egg__word');
var min = +input.min;
var max = +input.max;
function word(m) { return m <= 3 ? 'Runny' : m <= 5 ? 'Soft' : m <= 7 ? 'Jammy' : m <= 9 ? 'Medium' : 'Hard'; }
function update() {
var m = +input.value;
// The yolk sets from the outside in; CSS reads it all from this one value.
root.style.setProperty('--slider-egg-v', ((m - min) / (max - min)).toFixed(4));
number.textContent = m;
label.textContent = word(m);
input.setAttribute('aria-valuetext', m + ' minutes, ' + word(m).toLowerCase());
}
input.addEventListener('input', update);
update();
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.slider-egg').forEach(initSliderEgg);Goal: Create a "Egg" slider.
Visual behavior: Boiling time for an egg: the yolk goes from runny and glossy to jammy to firm and pale as the minutes go up.
Technique:
- One real <input type="range"> lies transparent over the track (its thumb sized to the drawn handle, so the pointer lines up), and everything drawn is set from its value.
- The egg stands in a cup with its top sliced off and lying beside it. The yolk is a radial-gradient whose runny centre shrinks and set ring grows with the minutes.
- A gloss fades from the runny yolk, crumbs fade in on a hard one, a grey line appears round an overdone one, and only a runny yolk spills over the rim and runs down the shell.
- Steam rises from the cut, the handle is an egg, and the reading names the result.
Accessibility: A native range input named "Boiling time" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the steam stops.
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 "Egg" slider.
Visual behavior: Boiling time for an egg: the yolk goes from runny and glossy to jammy to firm and pale as the minutes go up.
Technique:
- One real <input type="range"> lies transparent over the track (its thumb sized to the drawn handle, so the pointer lines up), and everything drawn is set from its value.
- The egg stands in a cup with its top sliced off and lying beside it. The yolk is a radial-gradient whose runny centre shrinks and set ring grows with the minutes.
- A gloss fades from the runny yolk, crumbs fade in on a hard one, a grey line appears round an overdone one, and only a runny yolk spills over the rim and runs down the shell.
- Steam rises from the cut, the handle is an egg, and the reading names the result.
Accessibility: A native range input named "Boiling time" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the steam stops.
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
Boiling time for an egg: the yolk goes from runny and glossy to jammy to firm and pale as the minutes go up.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- state change, morph
- Techniques
- css variables, radial gradient, keyframes
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native range input named "Boiling time" with an aria-valuetext in words, so a screen reader announces the reading rather than a bare number; keyboard focus outlines the drawn handle. The drawing is aria-hidden. Under prefers-reduced-motion the steam stops.