Interaction blueprint
Favorite burst
A one-time burst rewards a high-intent action without obscuring the surrounding content.
Build it
Choose a target and adapt the source to your interface.
<button
class="selection-control"
type="button"
aria-pressed="false"
aria-label="Favorite burst"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
class="selection-heart"
d="M12 21 3.5 12.5C-2 7 5 0 12 7c7-7 14 0 8.5 5.5Z"
/>
<circle
class="selection-particle"
cx="12"
cy="12"
r="1"
style="--dx: 15px; --dy: 0px"
/>
<circle
class="selection-particle"
cx="12"
cy="12"
r="1"
style="--dx: 7.5px; --dy: 12.990381px"
/>
<circle
class="selection-particle"
cx="12"
cy="12"
r="1"
style="--dx: -7.5px; --dy: 12.990381px"
/>
<circle
class="selection-particle"
cx="12"
cy="12"
r="1"
style="--dx: -15px; --dy: 0px"
/>
<circle
class="selection-particle"
cx="12"
cy="12"
r="1"
style="--dx: -7.5px; --dy: -12.990381px"
/>
<circle
class="selection-particle"
cx="12"
cy="12"
r="1"
style="--dx: 7.5px; --dy: -12.990381px"
/>
</svg>
</button>
<script>
const control = document.querySelector('.selection-control');
control.addEventListener('click', () =>
control.setAttribute(
'aria-pressed',
String(control.getAttribute('aria-pressed') !== 'true'),
),
);
</script>
<style>
.selection-control {
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
padding: 10px;
border: 0;
border-radius: 6px;
background: transparent;
color: #175bc5;
cursor: pointer;
}
.selection-control svg {
width: 24px;
height: 24px;
overflow: visible;
}
.selection-control:focus-visible {
outline: 2px solid currentColor;
outline-offset: 4px;
}
.selection-outline {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linejoin: round;
}
.selection-heart {
fill: transparent;
stroke: currentColor;
stroke-width: 1.6;
stroke-linejoin: round;
transition: fill 130ms ease-out;
}
.selection-particle {
fill: currentColor;
opacity: 0;
}
.selection-control[aria-pressed='true'] .selection-heart {
fill: currentColor;
}
.selection-control[aria-pressed='true'] svg {
animation: selection-pop 420ms ease-out;
}
.selection-control[aria-pressed='true'] .selection-particle {
animation: selection-burst 420ms ease-out;
}
@keyframes selection-burst {
0%,
30% {
opacity: 0;
transform: translate(0);
}
31% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate(var(--dx), var(--dy));
}
}
@keyframes selection-pop {
0%,
100% {
transform: scale(1);
}
35% {
transform: scale(1.2);
}
}
@media (prefers-reduced-motion: reduce) {
.selection-control *,
.selection-control[aria-pressed='true'] * {
animation: none;
transition: none;
}
}
</style>Animation breakdown
Frame phases, motion intent, implementation constraints and accessible alternatives.
Rest: outlined heart.
0–130ms: heart scales to 1.2.
130–280ms: it fills and six particles radiate.
280–420ms: particles fade; heart settles.
Motion intent
A one-time burst rewards a high-intent action without obscuring the surrounding content.
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 user favourites an item.
- Timing
- 420ms
- Easing
- cubic-bezier(.16, 1, .3, 1)
Animated properties
Implementation notes
- Keep unsaved and favourited aligned to the same visual anchor.
- Drive the transition from semantic state, not a timer, when the trigger is interactive.
- Use scale, opacity, progress 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 unsaved and favourited 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.