Icon morph blueprint
Arrow → Checkmark
A directional arrow folds into a clear completed-step confirmation without replacing the control.
Build it
Choose a target and adapt the source to your interface.
<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.
Rest: arrow points right.
0–90ms: shaft shortens as the head folds inward.
90–210ms: the two check strokes draw from the shared centre.
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
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.