Balloon

No dependencies
Code format
React + Tailwind
const css = `
  @keyframes toggle-balloon-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2.5px); } }
  @keyframes toggle-balloon-flare { 0% { transform: scale(0.2, 0); } 25% { transform: scale(1.3, 1.6); } 60% { transform: scale(0.9, 1); } 80% { transform: scale(1.05, 1.15); } 100% { transform: scale(0.8, 0.7); } }
  .toggle-balloon { display: inline-flex; align-items: center; gap: 14px; color: light-dark(#42454c, #e5e6e9); font-size: 15px; cursor: pointer; user-select: none; }
  .toggle-balloon__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
  .toggle-balloon__art { display: block; width: 82px; height: 100px; overflow: visible; border-radius: 8px; }
  .toggle-balloon__input:focus-visible + .toggle-balloon__art { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 4px; }
  .toggle-balloon__ground { fill: light-dark(#dbdde1, #2e3035); }
  .toggle-balloon__shadow { fill: light-dark(#ebedf0, #232529); transform-box: fill-box; transform-origin: 50% 50%; transition: transform 1.4s ease-in-out, opacity 1.4s; }
  .toggle-balloon__envelope { fill: light-dark(#6d7179, #7d8087); }
  .toggle-balloon__gore { fill: none; stroke: light-dark(#cbced3, #393c41); stroke-width: 1.2; }
  .toggle-balloon__band { fill: light-dark(#50535b, #c6c8cd); }
  .toggle-balloon__rope { stroke: light-dark(#6d7179, #7d8087); stroke-width: 0.9; }
  .toggle-balloon__basket { fill: light-dark(#cbced3, #393c41); }
  .toggle-balloon__weave { stroke: light-dark(#dbdde1, #2e3035); stroke-width: 0.8; }
  .toggle-balloon__flame { fill: light-dark(#373a40, #f1f2f3); transform-box: fill-box; transform-origin: 50% 100%; transform: scale(0.2, 0); filter: drop-shadow(0 0 2px light-dark(#373a40, #f1f2f3)); }
  /* Parked, the envelope slumps a little on the ground; lit, it fills out first, then lifts. */
  .toggle-balloon__fill { transform-box: view-box; transform-origin: 40px 60px; transform: scale(1.05, 0.9); transition: transform 0.6s ease-out 0.5s; }
  .toggle-balloon__craft { transform: translateY(8px); transition: transform 1.4s cubic-bezier(0.55, 0, 0.4, 1); }
  .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__fill { transform: none; transition-delay: 0.1s; }
  .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__craft { transform: translateY(-6px); transition: transform 1.8s cubic-bezier(0.4, 0, 0.3, 1) 0.4s; }
  .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__float { animation: toggle-balloon-bob 2.6s ease-in-out 2.2s infinite; }
  .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__flame { animation: toggle-balloon-flare 1.1s ease-out forwards; }
  .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__shadow { transform: scale(0.55); opacity: 0.5; transition-delay: 0.4s; transition-duration: 1.8s; }
  @media (prefers-reduced-motion: reduce) {
    .toggle-balloon__fill, .toggle-balloon__craft, .toggle-balloon__shadow,
    .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__craft { transition: none; }
    .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__float { animation: none; }
    .toggle-balloon__input:checked + .toggle-balloon__art .toggle-balloon__flame { animation: none; transform: scale(0.8, 0.7); }
  }
`;

export function BalloonToggle() {
  return (
    <>
      <style>
        {css}
      </style>
      <label className="toggle-balloon">
        <input className="toggle-balloon__input" type="checkbox" role="switch" />
        <svg className="toggle-balloon__art" viewBox="0 0 80 98" aria-hidden="true">
          <rect className="toggle-balloon__ground" x="6" y="93" width="68" height="3" rx="1.5" />
          <ellipse className="toggle-balloon__shadow" cx="40" cy="92" rx="13" ry="2" />
          <g className="toggle-balloon__craft">
            <g className="toggle-balloon__float">
              <g className="toggle-balloon__fill">
                <path className="toggle-balloon__envelope" d="M40 6C58 6 66 20 64 34C62 46 50 54 46 61H34C30 54 18 46 16 34C14 20 22 6 40 6Z" />
                <path className="toggle-balloon__gore" d="M40 6V61M40 6C30 14 28 40 34 61M40 6C50 14 52 40 46 61M40 6C22 12 18 36 26 52M40 6C58 12 62 36 54 52" />
                <path className="toggle-balloon__band" d="M17 38Q40 46 63 38L62 42Q40 50 18 42Z" />
              </g>
              <path className="toggle-balloon__rope" d="M34 61L35.5 75M46 61L44.5 75" />
              <path className="toggle-balloon__flame" d="M40 74C37.5 71 38.5 68 40 64C41.5 68 42.5 71 40 74Z" />
              <rect className="toggle-balloon__basket" x="34" y="75" width="12" height="9" rx="1.5" />
              <path className="toggle-balloon__weave" d="M34 79H46M38 75V84M42 75V84" />
            </g>
          </g>
        </svg>
        <span>Lift off</span>
      </label>
    </>
  );
}

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

Install with shadcn

npx shadcn@latest add @movin/toggle-balloon
Source on GitHub

About this component

A lift off switch drawn as a hot air balloon: the burner flares, the slumped envelope fills out, then the balloon rises and bobs as its shadow shrinks; off lets it sink back down.

Dependencies
None
Difficulty
Intermediate
Triggers
click, state change, loop
Techniques
keyframes, css transition, svg
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A native checkbox with role="switch" inside a <label>, so Space toggles it and the visible text names it; :focus-visible on the input outlines the drawing, which is aria-hidden. Under prefers-reduced-motion it lifts at once and does not bob.

More toggles

See all
Bolt
Bubbler
Burner
Chess clock