Icon morph blueprint
Play → Pause
A media affordance changes state in place so playback feels immediate and reversible.
Build it
Choose a target and adapt the source to your interface.
<button
class="morph-control"
type="button"
aria-pressed="false"
aria-label="Play → Pause"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M7 4 L12 7 L12 17 L7 20 Z"
fill="currentColor"
stroke="none"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 7 L20 12 L20 12 L12 17 Z"
fill="currentColor"
stroke="none"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<script>
const control = document.querySelector('.morph-control');
const shapes = [
{ from: 'M7 4 L12 7 L12 17 L7 20 Z', to: 'M6 4 L10 4 L10 20 L6 20 Z' },
{ from: 'M12 7 L20 12 L20 12 L12 17 Z', to: 'M14 4 L18 4 L18 20 L14 20 Z' },
];
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: 180,
easing: 'cubic-bezier(0.2,0.8,0.2,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: one centred play triangle.
0–80ms: triangle narrows from its rear edge.
80–160ms: the new gap opens between two equal bars.
160–180ms: hold.
Motion intent
A media affordance changes state in place so playback feels immediate and reversible.
This is direct manipulation feedback, so it completes within the immediate-response window.
The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.
Motion specification
- Trigger
- Playback toggles.
- Timing
- 180ms
- Easing
- cubic-bezier(.2, .8, .2, 1)
Animated properties
Implementation notes
- Keep play and pause 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 play and pause 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.