Icon morph blueprint

Sun ↔ Moon

A theme control makes the mode change visible through a calm celestial transformation.

Playground

Playhead 0ms / 360ms

Build it

Choose a target and adapt the source to your interface.

108 lines
index.html
<div class="theme-demo" data-dark="false">
  <button
    class="theme-control"
    type="button"
    aria-label="Dark appearance"
    aria-pressed="false"
  >
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <defs>
        <mask
          id="moon-cutout"
          maskUnits="userSpaceOnUse"
          x="0"
          y="0"
          width="24"
          height="24"
        >
          <rect width="24" height="24" fill="white" />
          <circle class="theme-cutout" cx="24" cy="0" r="5" fill="black" />
        </mask>
      </defs>
      <circle
        cx="12"
        cy="12"
        r="5"
        fill="currentColor"
        mask="url(#moon-cutout)"
      />
      <path
        class="theme-rays"
        d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5"
        fill="none"
        stroke="currentColor"
        stroke-width="1.6"
        stroke-linecap="round"
      />
    </svg>
  </button>
</div>

<script>
  const button = document.querySelector('.theme-control');
  button.addEventListener('click', () => {
    const dark = button.getAttribute('aria-pressed') !== 'true';
    button.setAttribute('aria-pressed', String(dark));
    document.querySelector('.theme-demo').dataset.dark = String(dark);
  });
</script>

<style>
  .theme-demo {
    display: inline-flex;
    padding: 24px;
    border-radius: 8px;
    background: #f4f6f9;
    color: #20252d;
    transition:
      background 360ms ease-out,
      color 360ms ease-out;
  }
  .theme-demo[data-dark='true'] {
    background: #19222f;
    color: #f4f6f9;
  }
  .theme-control {
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    padding: 10px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: inherit;
    cursor: pointer;
  }
  .theme-control svg {
    width: 24px;
    height: 24px;
  }
  .theme-rays {
    transform-origin: 12px 12px;
    transition:
      transform 160ms ease-out,
      opacity 160ms ease-out;
  }
  .theme-cutout {
    transition: transform 300ms cubic-bezier(0.32, 0.72, 0, 1);
  }
  .theme-control[aria-pressed='true'] .theme-rays {
    transform: scale(0.6) rotate(45deg);
    opacity: 0;
  }
  .theme-control[aria-pressed='true'] .theme-cutout {
    transform: translate(-7px, 7px);
  }
  .theme-control:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 4px;
  }
  @media (prefers-reduced-motion: reduce) {
    .theme-demo,
    .theme-rays,
    .theme-cutout {
      transition: none;
    }
  }
</style>

Animation breakdown

Frame phases, motion intent, implementation constraints and accessible alternatives.

360ms
01

Rest: circular sun with eight short rays.

02

0–160ms: rays retract and the disc shifts.

03

160–300ms: a dark mask creates the crescent.

04

300–360ms: the theme surface crossfades.

Motion intent

A theme control makes the mode change visible through a calm celestial transformation.

This duration leaves enough time to read the state change while keeping the interface responsive.

The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.

Motion specification

Trigger
The colour theme changes.
Timing
360ms
Easing
cubic-bezier(.32, .72, 0, 1)

Animated properties

transformopacity

Implementation notes

  • Keep sun and moon aligned to the same visual anchor.
  • Drive the transition from semantic state, not a timer, when the trigger is interactive.
  • Use transform, opacity and avoid layout work inside the animation loop.

Reduced motion

Swap to the final state instantly, preserving colour, label, and accessible state.

  • Use a minimum 44 by 44 pixel interactive target.
  • Expose the sun and moon states with aria-pressed, aria-expanded, or an updated accessible label.
  • Keep visible keyboard focus independent from the animation.

Avoid

  • Do not add bounce unless a real object pickup or spring settle is part of the interaction.
  • Do not hide the final state from assistive technology.
  • Do not make the user wait for animation before the underlying state updates.

Related blueprints

Browse all blueprints