Egg

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/slider-egg
Source on GitHub

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.

More sliders

See all
Flipbook
Grind
Hose
Nesting Dolls