Icon morph blueprint

Arrow → Checkmark

A directional arrow folds into a clear completed-step confirmation without replacing the control.

Playground

Playhead 0ms / 260ms

Build it

Choose a target and adapt the source to your interface.

76 lines
index.html
<button
  class="morph-control"
  type="button"
  aria-pressed="false"
  aria-label="Arrow → Checkmark"
>
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <path
      d="M5 12 L19 12 M13 6 L19 12 L13 18"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </svg>
</button>

<script>
  const control = document.querySelector('.morph-control');
  const shapes = [
    {
      from: 'M5 12 L19 12 M13 6 L19 12 L13 18',
      to: 'M5 12 L10 17 M10 17 L19 7 L19 7',
    },
  ];
  const paths = [...control.querySelectorAll('path')];
  let active = false;
  control.addEventListener('click', () => {
    active = !active;
    control.setAttribute('aria-pressed', String(active));
    paths.forEach((path, index) => {
      const start = active ? shapes[index].from : shapes[index].to;
      const end = active ? shapes[index].to : shapes[index].from;
      path.getAnimations().forEach((animation) => animation.cancel());
      path.setAttribute('d', end);
      if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      path.animate(
        [{ d: 'path("' + start + '")' }, { d: 'path("' + end + '")' }],
        {
          duration: 260,
          easing: 'cubic-bezier(0.22,1,0.36,1)',
        },
      );
    });
  });
</script>

<style>
  .morph-control {
    display: inline-grid;
    place-items: center;
    width: 44px;
    height: 44px;
    padding: 10px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: #20252d;
    cursor: pointer;
  }
  .morph-control svg {
    width: 24px;
    height: 24px;
    overflow: visible;
  }
  .morph-control:focus-visible {
    outline: 2px solid #175bc5;
    outline-offset: 4px;
  }
  @media (prefers-reduced-motion: reduce) {
    .morph-control {
      transition: none;
    }
  }
</style>

Animation breakdown

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

260ms
01

Rest: arrow points right.

02

0–90ms: shaft shortens as the head folds inward.

03

90–210ms: the two check strokes draw from the shared centre.

04

210–260ms: hold the confirmed state.

Motion intent

A directional arrow folds into a clear completed-step confirmation without replacing the control.

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
A form submission resolves successfully.
Timing
260ms
Easing
cubic-bezier(.22, 1, .36, 1)

Animated properties

transformopacity

Implementation notes

  • Keep arrow and check 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 arrow and check 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