Icon morph blueprint

Bookmark outline ↔ filled

A small save action becomes legible through a restrained fill, then holds a clear saved state.

Playground

Playhead 0ms / 240ms

Build it

Choose a target and adapt the source to your interface.

79 lines
index.html
<button
  class="selection-control"
  type="button"
  aria-pressed="false"
  aria-label="Bookmark outline ↔ filled"
>
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <path class="selection-fill" d="M7 3h10v18l-5-3-5 3Z" />
    <path class="selection-outline" d="M7 3h10v18l-5-3-5 3Z" />
  </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-fill {
    fill: currentColor;
    clip-path: inset(0 0 100%);
    transition: clip-path 150ms cubic-bezier(0.16, 1, 0.3, 1);
  }
  .selection-control[aria-pressed='true'] .selection-fill {
    clip-path: inset(0);
    transition-delay: 90ms;
  }
  .selection-control[aria-pressed='true'] svg {
    animation: selection-pop 240ms ease-out;
  }
  @keyframes selection-pop {
    0%,
    100% {
      transform: scale(1);
    }
    35% {
      transform: scale(1.08);
    }
  }
  @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.

240ms
01

Rest: outlined bookmark.

02

0–90ms: icon scales to 1.08.

03

90–190ms: fill sweeps down while the icon returns to scale 1.

04

190–240ms: hold the filled state.

Motion intent

A small save action becomes legible through a restrained fill, then holds a clear saved state.

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 saves or removes an item.
Timing
240ms
Easing
cubic-bezier(.16, 1, .3, 1)

Animated properties

transformopacity

Implementation notes

  • Keep save and saved 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 save and saved 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