コハム

Webクリエイターのコハムが提供する、Web制作に役立つ情報をまとめたブログです。

@propertyをフル活用!スクロールで変化する幻想的なCSSクリッピングアニメーション

Animating clip paths on scroll with @property in CSS

記事は上記記事を意訳したものです。

※当ブログでの翻訳記事は元サイト様に許可を得て掲載しています。


CSSを実験していると、もっと活用したいと思うテクニックを発見することがあります。これは私にとってまさにそんな発見でした...現在ではスクロールに合わせてクリップパスをアニメーション化できるようになり、とても気に入っています。画像を星型にアニメーション化したり、スクロールでポラロイド風の画像を作成したりできます。この記事では、クリップパス、@property、そしてコンテナユニットを使用して、視覚的に魅力的なスクロール駆動アニメーションを作成するためのテクニックをいくつかデモしたいと思います。

CSSには多くの新機能が登場していますが、私がよく考えるのは、それらすべてを組み合わせる方法です。これらすべての機能で、どのように実用的なユースケースを作成できるでしょうか?そのため、この記事では多くの要素を見ていきます:スクロール駆動アニメーション、@property、コンテナユニット、クランプ、その他の便利な機能です。モダンなCSSを書く時が来ました。

クリッピングとスクロールアニメーション

大きなデモに入る前に、まずは小さなデモから始めて、後で使用する基本的なテクニックをいくつか紹介しましょう。スクロール駆動アニメーションの基本については既に書いていますので、クリップパスのスクロールアニメーションについて直接説明していきましょう。これが最初に作成するエフェクトです:

Your browser does not support the video tag.

このデモでは、画像を画面の中央に固定しています。デモのために、の高さを300vhに設定して、スクロールできるオーバーフローを作成しています。これが画像に適用されるCSSで、下の3つのプロパティが今回使用するものです:

img {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: block;
  width: 100%;
  max-width: 50vmin;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border: 5px solid deeppink;

  clip-path: circle(20% at 0% 0%);
  animation: rotateOrb linear both;
  animation-timeline: scroll();
}

クリップパスをアニメーション化する場合、2つのオプションがあります。

オプション1:プロパティを直接アニメーション化する

最も簡単な方法は、プロパティを繰り返しアニメーション化することです。この方法を選択した場合、以下のようなアニメーションになります:

@keyframes rotateOrb {
  0% {
    clip-path: circle(20% at 0% 0%);
  }
  25% {
    clip-path: circle(20% at 100% 0%);
  }
  50% {
    clip-path: circle(20% at 100% 100%);
  }
  75% {
    clip-path: circle(60% at 0% 100%);
  }
  100% {
    clip-path: circle(150% at 0% 0%);
  }
}

批判を受ける前に言っておきますが:はい、このエフェクトを作成するのに必要な最小限のコードです。しかし、私にとってはこれは特に大きなアニメーションを使用する際に冗長に感じます。円は関係する値が3つだけなので理解しやすいですが、ポリゴンを使用する場合は複雑になります。もっと理解しやすい方法を使用できると便利です。これが次のパートにつながります。

オプション2:カスタムプロパティを使用する

これをもう少し読みやすくし、調整もしやすくするために、カスタムプロパティを使用してみましょう。まずコードを更新して、clip-pathプロパティの代わりに変数をアニメーション化するようにします:

img {
  /* 位置指定のプロパティ */
  clip-path: circle(var(--scale) at var(--move-x) var(--move-y));
  animation: rotateOrb linear both;
  animation-timeline: scroll();
}

@keyframes rotateOrb {
  0% {
    --move-x: 0%;
    --move-y: 0%;
  }
  25% {
    --move-x: 100%;
    --move-y: 0%;
  }
  50% {
    --move-x: 100%;
    --move-y: 100%;
  }
  75% {
    --move-x: 0%;
    --move-y: 100%;
    --scale: 60%;
  }
  100% {
    --move-x: 0%;
    --move-y: 0%;
    --scale: 150%;
  }
}

あとはルート内に基本変数を設定するだけだと思うかもしれません(ネタバレですが、これは機能しません):

:root {
  --move-x: 0%;
  --move-y: 0%;
  --scale: 20%;
}

これは以下のような「ガタガタした効果」を生みます:

Your browser does not support the video tag.

なぜこれは機能しないのか?

この時点で、ブラウザはこれらのプロパティをパーセンテージとしてアニメーション化すべきだということを認識していません。カスタムプロパティには何でも入れることができるからです。幸いなことに、すべての主要なブラウザで利用可能な@propertyという構文があり、これが解決に役立ちます。

ルート内にカスタムプロパティを追加する代わりに、@propertyを使用して追加してみましょう:

@property --move-x {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

@property --move-y {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

@property --scale {
  syntax: "<percentage>";
  initial-value: 20%;
  inherits: false;
}

これで、デモが完全に機能するようになります。

結論として:はい、コードは少し長くなりますが、より賢く感じられ、より多くのコントロールと簡単な調整が可能になると思います。上のCodePenでは、画像に.square-inクラスを与えることで、同じカスタムプロパティを使用する新しいアニメーションをトリガーできます。そうですね、このような小さなデモでは少し過剰かもしれません。しかし、大きな視点で見ると、特にプロジェクトで複数のアニメーションを使用する場合、維持がはるかに容易になります。

もう1つの簡単な例として、ポリゴン星をアニメーション化する例を示します:

なぜこれを作ったのかわかりませんが、必要な場合に備えて作っておきました:) @propertyについてさらに情報が必要な場合は、別の記事も書いています。

さて、信じられないかもしれませんが、これは導入部分でした。このテクニックを見たところで、いよいよ実際のデモを作成していきましょう:

スクロール駆動アニメーション、@property、クリップパスを使用したCSSジャーナルのアニメーション化

このステップバイステップのチュートリアルでは、CSSジャーナルのようなものを作成します。最近、実生活でジャーナルをつけることを考えていたので、そこからインスピレーションを得ました。基本的なアイデアは、画像と引用を並べて配置し、ビューポートに入る際にクールな演出を加えることです。最終的なデモでは、ユーザーにスクロールを促す小さな導入部分もありますが、それはデモの本質的な部分ではありません。

Your browser does not support the video tag.

CSSジャーナルのHTML構造

まずはHTMLの設定から始めましょう。画像とテキストを交互にDOM内に配置します。実際のコンテンツは除いて、以下がHTMLの構造です:

<section>
  <article>
    <figure>
      <img src="..." alt="..." />
      <figcaption>...</figcaption>
    </figure>
    <div class="entry">
      <p>...</p>
    </div>
  </article>
  <article>
    <div class="entry">
      <p>...</p>
    </div>
    <figure>
      <img src="..." alt="..." />
      <figcaption>...</figcaption>
    </figure>
  </article>
  <!-- これを数回繰り返します -->
</section>

このデモでのHTML要素の役割を見ていきましょう:

  • <section>: 記事を含み、フレックスカラムで表示します
  • <article>: アイテムのラッパーとして機能し、グリッドレイアウトを追加します
  • <figure>: 画像を保持し、ビューポートに入る際に少し回転します
  • <img>: ポラロイドフィルム効果を作成するためにアニメーション化されたクリップパスを適用します
  • <figcaption>: ポラロイドの下部に説明文として使用します
  • .entry: ビューポートに入る際にフェードインし、最初に行が表示され、その後テキストが表示されます

基本的なレイアウト - リセットと要素の整理

まず、簡単なリセットが必要です。見た目のために Google フォントも追加しました:

body {
  margin: 0;
  font-family: "Annie Use Your Telescope", cursive;
  background: radial-gradient(ivory 70%, transparent 30%),
  radial-gradient(ivory 70%, transparent 30%), #f5f3f3;
  background-size: 5px 5px;
}

figure {
  padding: 0;
  margin: 0;
}

img {
  display: block;
  width: 100%;
  max-width: 100%;
}

次に、sectionをフレックスカラムにし、アイテム間に間隔を設定します。これは後でスクロールアニメーションの体験を向上させます。また、すべての要素のスクロールアニメーションを見やすくするためにpadding-block: 50vhを設定します:

section {
  display: flex;
  flex-direction: column;
  gap: 40vmax;
  margin-inline: auto;
  padding-inline: 10vmax;
  padding-block: 50vh;
  max-width: 1400px;
}

次に、すべての<article>要素をグリッドに変換します(ブレークポイントは750pxに設定しましたが、自由に選択できます)。画像の幅を2/5に設定します。:has()を使用して画像の位置を確認し、それに基づいてグリッドを変更します:

article {
  display: grid;
  align-items: center;
  gap: 8vmax;
  @media (min-width: 750px) {
    grid-template-columns: 2fr 3fr;
    gap: 10vmax;
  }
  &:has(.entry + figure) {
    @media (min-width: 750px) {
      grid-template-columns: 3fr 2fr;
    }
  }
}

エントリーのフェードアニメーション

ここで最初のスクロール駆動アニメーションを実装します。エントリーを少し上方向にアニメーション化しながら、テキストの色を透明から黒に変化させます。opacityではなくcolorをアニメーション化するのは、opacityプロパティを行のために使用したいからです。前述の通り、「.entryはビューポートに入る際にフェードインし、最初に行が表示され、その後テキストが表示される」という動きを実現します。

.entry {
  animation: revealEntry linear both;
  animation-timeline: view(block);
  animation-range: cover 20% contain 40%;
}

@keyframes revealEntry {
  from {
    color: transparent;
    transform: translateY(20%);
  }
  to {
    color: black;
    transform: translateY(0%);
  }
}

これらのエフェクトの完璧なアニメーション範囲を見つける方法について疑問に思うかもしれません。通常、私はタブを開いてBramusが作成した範囲ツールで試してみます。Chrome用のプラグインも作成していますが、私のワークフローではこちらの方が好みです。もちろん選択は自由です。

次に、テキストの行を作成し、少し早めに表示させます。これは単純なフェードインアニメーションです。以下が完全なコードです:

.entry p {
  font-size: clamp(1.25rem, 0.9891rem + 1.3043vi, 2rem);
  background-image: repeating-linear-gradient(
    to top,
    #95d9c3 0 0.125rem,
    transparent 0.125rem 1lh
  );
  background-position-y: 0.8lh;

  animation: fadein linear both;
  animation-timeline: view(block);
  animation-range: entry 10% contain 10%;
}

@keyframes fadein {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

行はrepeating-linear-gradientを使用して作成され、lhユニット(現在のフォントの行の高さに基づく相対的なCSSユニット)を使用しています。これはテキストのリサイズに便利で、特にclamp()を使用した流動的なタイポグラフィを使用している場合に有効です。最近、CSSの相対的な長さユニットについてのシリーズを書きましたので、これらのテクニックについてもっと読みたい場合はそちらをご覧ください。ただし、このデモではおまけとして追加しています。

現時点で、以下のような状態になっているはずです:

Your browser does not support the video tag.

画面に入る際のfigureの回転

クリッピングに進む前に、<figure>の回転から始めましょう。これを行うために、デフォルトのスタイリングを追加し、aspect-ratio: 1を設定します。このような写真は通常正方形のアスペクト比なので、これは理にかなっています:

figure {
  --rotate: 20deg;

  position: relative;
  aspect-ratio: 1;
  background: white;
  box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;

  animation: rotate linear both;
  animation-timeline: view(block);
  animation-range: cover 20% contain 30%;
  @media (max-width: 749px) {
    max-width: 350px;
    align-self: center;
  }
}

figureの回転を制御するために--rotateカスタムプロパティも追加しました。このカスタムプロパティをキーフレームで使用します:

@keyframes rotate {
  entry 100%,
  exit 0% {
    opacity: 1;
    rotate: var(--rotate);
  }
  exit 100% {
    opacity: 0;
    rotate: 0deg;
  }
}

素晴らしい!デモを実際に作成している場合、すべてのfigureが同じ方向に回転していることに気付くかもしれません。ここで:has()が役立ちます!articleの以下の行を覚えていますか?それを更新してみましょう:

&:has(.entry + figure) {
  @media (min-width: 750px) {
    grid-template-columns: 3fr 2fr;
  }
  figure {
    --rotate: -20deg;
  }
}

これにより、画像がentryの後に配置されている場合、反対方向に回転するようになります。 :has()は本当に素晴らしい機能です!

クリップパスと@propertyを使用してポラロイド効果を作成する

フレームを作成するには、2つの値だけをアニメーション化する必要があります。フレームには3つの等しい幅の辺があり、下部だけが広くなっているためです。クリッピング用に2つのカスタムプロパティを作成し、初期値を0%に設定します:

@property --clip-1 {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

@property --clip-2 {
  syntax: "<percentage>";
  initial-value: 0%;
  inherits: false;
}

画像には、デフォルトのスタイリングを追加し、aspect-ratio: 1を設定しながら、insetでクリッピングします。insetクリッピングの値は馴染みのあるもので、マージンと同じように:上、左、下、右の順です。したがって、以下のように変換されます:

inset(var(--clip-1) var(--clip-1) var(--clip-2) var(--clip-1));

以下が画像の完全なスタイリングです:

img {
  aspect-ratio: 1;
  object-fit: cover;

  clip-path: inset(var(--clip-1) var(--clip-1) var(--clip-2) var(--clip-1));
  animation: createFrame linear both;
  animation-timeline: view(block);
  animation-range: cover 15% contain 40%;
}

アニメーションでは、これらの値を更新し、シャドウを追加します:

@keyframes createFrame {
  to {
    --clip-1: 5%;
    --clip-2: 15%;
    box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px,
        rgba(0, 0, 0, 0.3) 0px 3px 7px -3px;
  }
}

キャプションをフレーム内にアニメーション化する

<figcaption>を絶対位置指定し、text-overflow: ellipsisを使用して最大幅も設定します。しかし、フレームはウィンドウの幅に応じて変化するため、コンテナに基づいてキャプションのサイズと位置を決めると面白いかもしれません。

そのために、figureをコンテナにしましょう:

figure {
  container: frame;
  container-type: size;
  /* 以前のコード... */
}

通常、container-typeをsizeに設定すると、ブロック軸を使用する場合にコンテナに何らかの定義された高さが必要なため問題になります。しかし幸運なことに、aspect-ratioのおかげでこれは既に設定されています。

figcaption {
  position: absolute;
  inset-inline: 5cqw;
  bottom: 1.2cqh;
  font-size: 8cqh;
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
  z-index: 1;
}

あとは、テキストを表示させるだけです。既に作成したカスタムプロパティを使用して、再度insetクリップパスでアニメーション化します:

figcaption {
  --clip-1: 100%;

  /* 前述のプレゼンテーションスタイル */
  clip-path: inset(0 var(--clip-1) 0 0);
  animation: revealText linear both;
  animation-timeline: view(block);
  animation-range: cover 25% contain 35%;
}

@keyframes revealText {
  to {
    --clip-1: 0%;
  }
}

ユーザー設定とブラウザサポートに注意する

このデモの最後に、常にユーザー設定とブラウザサポートについて考慮する必要があることを指摘しておきます。

ベストプラクティスとして、これをプログレッシブエンハンスメントとして実装すべきです。しかしデモの理由から、デモを過度に複雑にしないように、notオペレータをサポートフラグで使用することにしました:

@supports not (animation-timeline: view(block)) {
  .intro {
    animation: none;
  }

  section * {
    animation: none;
  }

  figure {
    rotate: var(--rotate);
  }

  figcaption {
    --clip-1: 0%;
  }
  img {
    --clip-1: 5%;
    --clip-2: 15%;
    box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px,
        rgba(0, 0, 0, 0.3) 0px 3px 7px -3px;
  }
}

モーション軽減を好むユーザーに対しても同じアプローチを使用できます。この場合、同じコードを以下の中に入れることができます:

@media (prefers-reduced-motion) {
    /* サポートクエリと同じ内容 */
}

最終結果

記事に従ってきた場合、このページに埋め込まれているデモと同様の結果が得られているはずです。

最終版では、簡単なグループ化のためにカスケードレイヤーも追加し、小さな導入部分も加えましたが、それが唯一の違いです。

私はこのモダンなCSS機能のショーケースを楽しんでいただけたと思います。このデモを作成するのはとても楽しく、スクロール駆動アニメーションとクリップパスを組み合わせて他の人々がどんな魔法のようなものを作り出すのか楽しみです。HTMLとWebコンポーネントに主に取り組んでいた後、CSSに少し戻ってくるのは良かったです。本当に楽しみました。一つの疑問が残ります:実際の物理的なジャーナルを作成して「ジャーナリング」を始めるでしょうか?まだ確信が持てませんが、少なくともこの小さなデモはそのアイデアから生まれました。

©コハム