Interaction blueprint

Favorite burst

A one-time burst rewards a high-intent action without obscuring the surrounding content.

Playground

Playhead 0ms / 420ms

Build it

Choose a target and adapt the source to your interface.

145 lines
index.html
<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.

420ms
01

Rest: outlined heart.

02

0–130ms: heart scales to 1.2.

03

130–280ms: it fills and six particles radiate.

04

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

scaleopacityprogress

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.

Related blueprints

Browse all blueprints