Anchor interpolated morphing

A pure CSS technique combining anchor(), anchor-size(), @starting-style, and interpolate-size to create smooth contextual transitions from a trigger element to an overlay. Based on Adam Argyle's AIM technique.

html #ui#animation
<style>
  :root {
    --ease-3: cubic-bezier(0.25, 0, 0.3, 1);
    --ease-spring-3: linear(
      0,
      0.009,
      0.035 2.1%,
      0.141,
      0.281 6.7%,
      0.723 12.9%,
      0.938 16.7%,
      1.017,
      1.077,
      1.121,
      1.149 24.3%,
      1.159,
      1.163,
      1.161,
      1.154 29.9%,
      1.129 32.8%,
      1.051 39.6%,
      1.017 43.1%,
      0.991,
      0.977 51%,
      0.974 53.8%,
      0.975 57.1%,
      0.997 69.8%,
      1.003 76.9%,
      1.004 83.8%,
      1
    );
  }

  body {
    margin: 0;
    display: grid;
    place-items: center;
    min-height: 100vh;
    font-family:
      system-ui,
      -apple-system,
      sans-serif;
    color: #2f2f37;
  }

  .triggers {
    display: flex;
    gap: 24px;
    flex-wrap: wrap;
    justify-content: center;
    padding: 24px;
  }

  button {
    font-family: inherit;
    font-size: 15px;
    font-weight: 500;
    padding: 12px 24px;
    color: #2f2f37;
    cursor: pointer;
    transition:
      background 0.15s,
      border-color 0.15s;
    border: 1px solid #e0ddd8;
    border-radius: 16px;
    background: white;
    box-shadow:
      0 24px 48px rgba(0, 0, 0, 0.12),
      0 4px 12px rgba(0, 0, 0, 0.06);
  }

  button:hover {
    background: #f0efec;
    border-color: #ccc9c2;
  }

  button:active {
    scale: 0.97;
  }

  /* Each button is its own anchor, each dialog targets its trigger */
  .trigger-1 {
    anchor-name: --morph-1;
  }
  .trigger-2 {
    anchor-name: --morph-2;
  }
  .trigger-3 {
    anchor-name: --morph-3;
  }

  #dialog-1 {
    position-anchor: --morph-1;
  }
  #dialog-2 {
    position-anchor: --morph-2;
  }
  #dialog-3 {
    position-anchor: --morph-3;
  }

  dialog {
    --_size: min(420px, 90vw);
    --_speed-in: 0.3s;
    --_speed-out: 0.2s;
    --_speed: var(--_speed-in);
    --_anchor-radius: 16px;
    /* polish+ dur, ease and delay for timing the dialog bg with the morph */
    --_bg-transition: calc(var(--_speed) * 0.25) var(--ease-3) 0s;
    /* polish+ top has different timing/easing when going in vs out */
    --_top-transition: calc(var(--_speed) * 2.5) var(--ease-spring-3);

    interpolate-size: allow-keywords;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 16px;
    background: white;
    color: #2f2f37;
    box-shadow:
      0 24px 48px rgba(0, 0, 0, 0.12),
      0 4px 12px rgba(0, 0, 0, 0.06);
    /* mask effect — clip content as dialog grows/shrinks */
    overflow: clip;
    position: fixed;
    inset: unset;

    /* ON STAGE position: near anchor with offset */
    left: calc(anchor(left) - (var(--_size) * 0.25));
    top: calc(anchor(top) - 25px);

    @media (prefers-reduced-motion: no-preference) {
      transition:
        display var(--_speed) allow-discrete,
        overlay var(--_speed) allow-discrete,
        height var(--_speed) var(--ease-3),
        width var(--_speed) var(--ease-3),
        top var(--_top-transition),
        right var(--_speed) var(--ease-3),
        bottom var(--_speed) var(--ease-3),
        left var(--_speed) var(--ease-3),
        border-radius var(--_speed) var(--ease-3),
        background var(--_bg-transition);
    }

    > .dialog-content {
      /* fixed width for the mask/clip reveal effect */
      inline-size: var(--_size);

      @media (prefers-reduced-motion: no-preference) {
        transition:
          opacity var(--_speed) var(--ease-3),
          transform var(--_speed) var(--ease-3);
      }

      /* slide in from top */
      @starting-style {
        transform: translateY(-50px);
      }
    }

    /* EXIT STAGE TO — collapse back to anchor */
    &:not([open]) {
      left: anchor(left);
      top: anchor(top);
      right: anchor(right);
      bottom: anchor(bottom);
      width: anchor-size(width);
      height: anchor-size(height);
      background-color: #0000;
      border-radius: var(--_anchor-radius);

      /* outgoing timing mods */
      --_speed: var(--_speed-out);
      --_bg-transition: var(--_speed) var(--ease-3) calc(var(--_speed) * 0.5);
      --_top-transition: var(--_speed) var(--ease-3);

      > .dialog-content {
        opacity: 0;
      }
    }

    /* ENTER STAGE FROM — start at anchor shape */
    @starting-style {
      &[open] {
        left: anchor(left);
        top: anchor(top);
        right: anchor(right);
        bottom: anchor(bottom);
        width: anchor-size(width);
        height: anchor-size(height);
        background-color: #0000;
        border-radius: var(--_anchor-radius);

        > .dialog-content {
          opacity: 0;
        }
      }
    }

    &::backdrop {
      opacity: 0;
    }
  }

  .dialog-content {
    padding: 24px;
  }

  .dialog-content h2 {
    margin: 0 0 8px;
    font-size: 18px;
    font-weight: 600;
  }

  .dialog-content p {
    margin: 0 0 16px;
    font-size: 14px;
    line-height: 1.6;
    color: #737373;
  }

  .dialog-content form {
    margin: 0;
  }

  .dialog-content .close-btn {
    display: block;
    width: 100%;
    padding: 10px;
    border-radius: 8px;
    background: #2f2f37;
    color: white;
    border: none;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    box-shadow: none;
  }

  .dialog-content .close-btn:hover {
    background: #1a1a1f;
  }

  .dialog-header {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
  }

  .dialog-icon {
    width: 40px;
    height: 40px;
    border-radius: 10px;
    display: grid;
    place-items: center;
    font-size: 20px;
    flex-shrink: 0;
  }

  .icon-purple {
    background: #ede9fe;
  }
  .icon-blue {
    background: #dbeafe;
  }
  .icon-amber {
    background: #fef3c7;
  }
</style>

<div class="triggers">
  <button class="trigger-1" commandfor="dialog-1" command="show-modal">
    Details
  </button>
  <button class="trigger-2" commandfor="dialog-2" command="show-modal">
    Preview
  </button>
  <button class="trigger-3" commandfor="dialog-3" command="show-modal">
    Settings
  </button>
</div>

<dialog id="dialog-1" closedby="any">
  <div class="dialog-content">
    <div class="dialog-header">
      <div class="dialog-icon icon-purple">📋</div>
      <div><h2>Details</h2></div>
    </div>
    <p>
      This dialog morphed from the button that triggered it using pure CSS
      anchor positioning and interpolate-size. No JavaScript layout calculations
      needed.
    </p>
    <form method="dialog"><button class="close-btn">Close</button></form>
  </div>
</dialog>

<dialog id="dialog-2" closedby="any">
  <div class="dialog-content">
    <div class="dialog-header">
      <div class="dialog-icon icon-blue">👁</div>
      <div><h2>Preview</h2></div>
    </div>
    <p>
      The entry and exit animations follow a curved path from the anchor
      element, unlike View Transitions which interpolate in a straight line.
    </p>
    <form method="dialog"><button class="close-btn">Close</button></form>
  </div>
</dialog>

<dialog id="dialog-3" closedby="any">
  <div class="dialog-content">
    <div class="dialog-header">
      <div class="dialog-icon icon-amber">⚙️</div>
      <div><h2>Settings</h2></div>
    </div>
    <p>
      The animation is interruptible — click a different button while this
      dialog is open and it will smoothly redirect to the new anchor position.
    </p>
    <form method="dialog"><button class="close-btn">Close</button></form>
  </div>
</dialog>