clip-path と CSS Masks でポップアップをアイリスワイプさせてみた


ポップアップの表示/非表示アニメーションはフェードやスライドで作るのが一般的ですが、 clip-path や CSS Masks を使えば同心円状のワイプ(アイリスワイプ)も作れそう…作れました。つまりこういうことです。

コミカルでポップなテイストのデザインに合いそうですね。
動くデモはこちら。

説明

  • clip-path バージョン
    ポップアップを丸いパス( clip-path: circle(); )で切り抜き、パスのサイズをアニメーションさせています。

  • mask バージョン
    ポップアップに丸いマスク画像(実際は円形グラデーション)を適用し、マスクのサイズをアニメーションさせています。

後は面倒なのでコードを読んでください(ぶん投げ)。長いので clip-path バージョンのみ掲載します。
あ、 💬 の付いたコメントが今回の肝です。そこだけ読めば良いかと。
GitHub はこちら。

<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Iris Wipe Popup with clip-path</title>
<style>

  * {
    line-height: 1;
    margin: 0;
    padding: 0;
  }
  :root {
    font-size: 5vmin;
  }
    body {
      background-image: repeating-linear-gradient(-45deg, #0070e0, #0070e0 16px, #0080f0 16px, #0080f0 32px);
      color: #ffffff;
      font-family: "Arial Black", "Avenir-Black";
      -webkit-font-smoothing: antialiased;
      overflow-y: scroll;
      -webkit-tap-highlight-color: transparent;
    }
      article {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 100%;
      }
        h1 {
          font-size: 2.0rem;
          line-height: 1.375;
          margin: 0 1.0rem 2.0rem;
          text-align: center;
        }
        button {
          background-color: rgba(0, 0, 0, 0.1);
          border: 4px solid #ffffff;
          color: #ffffff;
          cursor: pointer;
          font-family: "Arial Black", "Avenir-Black";
          font-size: 1.0rem;
          outline: none;
          padding: 0.5rem 2.0rem;
          text-transform: uppercase;
        }
        button:hover {
          background-color: rgba(255, 255, 255, 0.1);
        }
      .popup {
        background-image: repeating-linear-gradient(45deg, #e02000, #e02000 16px, #f03000 16px, #f03000 32px);

        /* 💬 クリッピングされているため、 box-shadow のようなボックス外部に適用されるスタイルは描画されない */
        /* box-shadow: 4px 4px 32px 16px rgba(0, 0, 0, 0.25); */

        display: none;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        margin: auto;
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        width: 90vmin;
        height: 90vmin;

        /* 💬 丸いクリップパスを適用する */
        /* 💬 プリフィクスは Safari 用 */
        -webkit-clip-path: circle(50%);
        clip-path: circle(50%);

        /* 💬 矩形バージョン
        -webkit-clip-path: inset(0 0 0 0);
        clip-path: inset(0 0 0 0);
        */

      }
      .popup.open {
        animation: popup-animation 750ms ease-in-out 0ms 1;
        display: flex;
      }
      .popup.close {
        animation: popup-animation 750ms ease-in-out 0ms 1 reverse;
        display: flex;
      }

      /* 💬 クリップパスのサイズをアニメーションさせる */
      /* 💬 `clip-path: circle();` は transition できない。バグ? */
      @keyframes popup-animation {
        0% {
          -webkit-clip-path: circle(0%);
          clip-path: circle(0%);

          /* 💬 矩形バージョン
          -webkit-clip-path: inset(50% 50% 50% 50%);
          clip-path: inset(50% 50% 50% 50%);
          */

        }
        100% {
          -webkit-clip-path: circle(50%);
          clip-path: circle(50%);

          /* 💬 矩形バージョン
          -webkit-clip-path: inset(0 0 0 0);
          clip-path: inset(0 0 0 0);
          */

        }
      }

</style>
<article>
  <h1>Iris Wipe Popup with clip-path</h1>
  <button onclick="openPopup()">Open popup!</button>
</article>
<aside class="popup">
  <h1>🎉 Popup! 🎉</h1>
  <button onclick="closePopup()">Close popup!</button>
</aside>
<script>
  const popup = document.querySelector('.popup')
  const openPopup = () => {
    popup.classList.add('open')
    popup.classList.remove('close')
  }
  const closePopup = () => {
    popup.classList.remove('open')
    popup.offsetWidth
    popup.classList.add('close')
    popup.addEventListener('webkitAnimationEnd', function () {
      popup.removeEventListener('webkitAnimationEnd', arguments.callee)
      popup.classList.remove('close')
    })
  }
</script>

なお、実際にプロダクトで使う際はバックドロップ( i.e. dialog::backdrop 疑似要素 )のスタイライズも必要になってくるでしょうから、ポップアップの DOM 構造は二重三重の入れ子にした方が無難です。

ポイント

  • clip-path: circle(); が CSS Transition に対応していない
    transition で clip-path: polygon() をモーフィングさせるデモはよく見かけるんですが、 clip-path: circle() はどうも未対応のようです。 もっとも、 CSS Animation で代用できるので問題はないと思います。

  • -webkit-mask-size が CSS Transition に対応していない
    もしかして上記共々バグなのかしら?しかしこれも animation で代用できるからセーフ、と思いきや…

  • -webkit-mask-size が Safari で CSS Animation に対応していない
    アウト。 mask の対応状況は割と複雑 なんですが、少なくともデモの mask バージョンは Chrome と Firefox でしか機能しませんでした。

  • Edge は全滅
    clip-path バージョンも mask バージョンもワイプされませんでした。 どうもこういうことらしいです。 もう知らない!プンプン!

おわりに

とりあえず、「 mask は避け、 clip-path を活用する、ただし機能しないことを念頭にアクセシビリティは担保する 」という結論に至りました。おしまい。