CSSがようやくビジュアルインタラクションのコントロールを取り戻し、スタイリング、アニメーション、アクセシビリティを本来あるべき姿で担っているのが素晴らしいです。今日では、ネイティブブラウザの機能により、重い処理をJavaScriptのメインスレッドから離し、GPUに近づけることが可能になりました。ブラウザのエンジンにパフォーマンスを最適化させることで、エネルギーと処理能力を節約しながら、堅牢でアクセシブル、かつ将来廃止される可能性のある外部ライブラリに依存しないコードを構築できます。

3D、モダンなレイアウト手法、clip-path、transform、カスタムプロパティ、スクロール駆動アニメーション、view-transitions、@property — そしてほとんど何でもアニメーション化できます、auto-heightへのアニメーションも

もちろんSVGもあります。これは新しいものではありませんが、イラストレーションとアニメーションを通じてWebサイト全体を構築することを可能にします。以下の例を見てください:レスポンシブで軽量、アクセシブルで、主にCSS Grid + SVGで動いています。

SVGだけでUIを含むビデオゲーム全体を構築することもできます:

以下はモダンCSSの完全なガイドではありませんが、サイトを生き生きとさせ、記憶に残るものにしたいときに私が使うテクニックの、意見に基づく選定です。記憶に残る体験を作る方法はたくさんあります。スムーズに完了するフォームのようにシンプルなこともあります。しかしここでは、表現力豊かな側面に焦点を当てています。

コミュニケーションとしてのモーション:意図の定義

技術的な側面に入る前に、明確にしておきたいことがあります:できるからといって、ただ動かすべきではありません。

すべてがコミュニケーションであり、アニメーションも例外ではありません。意図を厳密に絞り込み、やり過ぎないようにするために、伝えたいメッセージをサポートする動きをデザインする時間を取る必要があります。

サイトのデザインとアニメーションを計画する際に私が使う方法論を紹介します。

キノコに焦点を当てた自然イベントのプロジェクトに取り組んでいると想像してください。デザイン言語は「雰囲気」によって大きく変わります。「サイケデリックなキノコのレイブ」を売ることは、祖先の薬に焦点を当てた「スピリチュアルなキノコのリトリート」とは全く異なります。

すべてのデザイン決定がコミュニケーションになります。私は自分の意図と範囲を定義するためにキーワードリストを作成するのが好きです。例えば、異なるオプションに分解してみましょう:

オプションA:サイケデリックなイベント

  • ビジュアル:カラフル、彩度が高く、コントラストが高く、イラスト、歪み
  • 動き:速い、慌ただしい、予測不能、変形する、リズミカル、同期したループ、催眠的
  • 感覚:楽しい、カオス、エネルギッシュ、刺激的、驚き
  • タイポグラフィ:ファンク、「サイケ・ロック」
  • スタイルリファレンス:ポップアート、60s/70sオプアート、レイブフライヤー
  • アクション:ダンス
  • その他:絵文字、映画(例:Fear and Loathing in Las Vegas

オプションB:スピリチュアルなリトリート

  • ビジュアル:アーストーン、ニュートラルトーン、彩度を抑えた、写真中心、自然、余白
  • 動き:遅い、流動的、有機的、呼吸する、微妙な視差、スムーズなスクロール。
  • 感覚:穏やか、静か、内省的、思索的、安全
  • タイポグラフィ:エレガントなセリフ体、ミニマリストのサンセリフ体、広い間隔、読みやすい
  • スタイルリファレンス:スカンジナビアデザイン、日本のわびさび、ウェルネス/スパの美学、植物図鑑
  • アクション:呼吸
  • その他:ヒーリングサウンド、映画(例:Eat Pray Love

これは私のデザインとアニメーションの決定を導くための演習です。このリストは、使用するCSSプロパティとその使い方を選択するのに役立ちます。クライアントとも共有し、一緒に方向性を選びます。

オプションAを選んだとして、記憶に残るユーザー体験を作るための必須要素の例をいくつか見てみましょう。

分割テキストアニメーション

これらのアニメーションはGSAP SplitTextプラグインのおかげで人気になりました。テキストを文字(または単語、行)ごとに分割して、スタガードアニメーションのような面白いテキストエフェクトを作成できます。

<h1 class="reveal-text">
  <span style="--i:0">H</span>
  <span style="--i:1">O</span>
  <span style="--i:2">L</span>
  <span style="--i:3">A</span>
</h1>

このアプローチは「Hola」の各文字をspanで囲みます。その後、各spanは順序を示すインデックス付きのカスタムプロパティでインラインスタイルが適用されます。これはsibling-index()関数が広くサポートされるようになれば、はるかに簡単になります。

しかし今は、各カスタムプロパティの値がanimation-delayを増加させる乗数として機能し、各spanをスタガリングします。このケースでは、各文字を上方向に移動しながらフェードインさせます。

.reveal-text span {
  animation: slideUp 0.6s ease-out forwards;
  animation-delay: calc(var(--i) * 0.1s);
  display: inline-block;
  opacity: 0;
  transform: translateY(3rem);
}

@keyframes slideUp {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

ここでの難点はアクセシビリティです。個々のspanをすべてaria-hidden="true"で支援技術から隠し、スクリーンリーダー用に完全な単語の視覚的に隠されたバージョンを追加したくなります:

<h1>
  <span class="sr-only">HOLA</span>
  <span aria-hidden="true" class="reveal-text">
    <span style="--i:0">H</span>
    <span style="--i:1">O</span>
    <span style="--i:2">L</span>
    <span style="--i:3">A</span>
  </span>
</h1>
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

しかし注意してください:このパターンはすべてのスクリーンリーダーで良好な体験を保証するものではありません。Adrian RoselliはGSAPのSplitTextを8つのスクリーンリーダーとブラウザの組み合わせでテストし、そのうち2つでのみ正しく動作することを発見しました。このテクニックを採用する場合は、実際の支援技術でテストしてください。

そのリスクが高すぎる場合は、Preethiによる非常に賢い代替案があり、letter-spacingプロパティを使用します。負の値を受け入れて文字を重ねて非表示にし、DOMに触れることなくアニメーション化できます。0に戻すと、アクセシビリティのオーバーヘッドなしに同様の表示エフェクトが得られます。

素晴らしいのは、::first-letterが最初の文字を選択するように、個々のグリフをCSSから直接ターゲットにする::nth-letterのような疑似セレクタがあれば、ということです。しかし残念ながら、::nth-letterはまだありません。

すべてのアニメーションでユーザーのモーションプリファレンスを尊重することを忘れないでください:

@media (prefers-reduced-motion: reduce) {
  .reveal-text span {
    animation: none; /* またはよりソフトなアニメーション */
  }
}

そしてこちらがその結果です:

多くのテキストがあり、異なるアニメーションを適用したい場合、規模が大きくなりすぎる可能性があります。サイケデリックなイベントでは、SMILでテキストを分割してみましたが、冗長でした。これは2文字だけをアニメーション化するコードです:

<svg role="img" aria-label="TODOS LOS HONGOS" viewBox="0 0 1366 938.96">
  <title>TODOS LOS HONGOS</title>
  <g aria-hidden="true">
    <text transform="rotate(-9.87 2181.107 -1635.1)" opacity="0">T
      <animate attributeName="dy" values="100; -20; 0" keyTimes="0; 0.8; 1" dur="0.4s" begin="0s" fill="freeze"/>
      <animate attributeName="opacity" from="0" to="1" dur="0.01s" begin="0s" fill="freeze"/>
    </text>
    <text transform="rotate(-8.92 2372.854 -2084.755)" opacity="0">O
      <animate attributeName="dy" values="100; -20; 0" keyTimes="0; 0.8; 1" dur="0.4s" begin="0.1s" fill="freeze"/>
      <animate attributeName="opacity" from="0" to="1" dur="0.01s" begin="0.1s" fill="freeze"/>
    </text>
    <!-- 残りの文字... -->
  </g>
</svg>

<svg>role="img"<title>を追加し、個々の文字を<g aria-hidden="true">で囲みます。これにより、スクリーンリーダーは1つのクリーンなラベルを読み取ることができます。一部の組み合わせではうまく動作しますが、他の組み合わせではうまく動作しないため、テキストが重要な場合はアニメーション化しないでください。

完全なコードはこちらです。AIにやってもらえば書きやすくなります:

より長いテキストの場合、GSAPのようなライブラリはより多くのコントロールを提供しますが、先ほど議論した同じアクセシビリティリスクが適用され、スクリーンリーダー間の結果は一貫性がありません:

<h1>
  <span class="splitfirst">Todos los hongos son</span>
  <span class="splitlast">mágicos</span>
</h1>
const splitFirst = SplitText.create('.splitfirst', {
  type: "chars",
});
const splitLast = SplitText.create('.splitlast', {
  type: "chars, lines",
  mask: "lines"
});

const tween = gsap.timeline()
.from(splitFirst.chars, {
  xPercent: 100,
  stagger: 0.1,
  opacity: 0,
  duration: 1, 
})
.from(splitLast.chars, {
  yPercent: 100,
  stagger: 0.1,
  opacity: 0,
  duration: 1,
});

オプションBを選択した場合、これは素敵なアプローチでしょう。テキストがフェードインする様子から、「穏やか」な感じが伝わってきます。

マスキングとクリッピング

clip-pathmaskプロパティは要素の一部を非表示にできますが、根本的に異なる原理で動作します。クリッピングは二値の決定です:ピクセルは完全に表示されるか完全に消えるかのどちらかで、ポリゴン、円、SVGパスなどのクリーンな幾何学的形状に適しており、ブラウザはレンダリングをより効率的に最適化できます。一方、マスキングは輝度またはアルファチャンネルの値を使用します:白は表示、黒は非表示、中間は部分的な透明度を生み出します。これにより、ソフトエッジ、グラデーションフェード、不規則なテクスチャに適したツールになります。非常に複雑なベクター形状がある場合、ベクターのclip-pathよりもmaskを使用する方がパフォーマンスが良い場合があることに注意してください。Sarah Drasnerは、どちらを使うのが適切かについての良い記事を書いています。

私たちのプロジェクトはclip-pathの非常に明確なユースケースです。円の形状があり、clip-path: circle(0%)から始まります。これにより要素が非表示になります(クリッピング円の半径がゼロ)。アニメーションの期間中、circle(100%)に拡大し、円が中心から外側に拡大するにつれて要素が完全に表示されます。同時にopacityの助けを借りてフェードインさせます。

#rainbow, #floor, #mushroom, #flores {
  opacity: 0;
  animation: maskAnim 2s ease-in forwards;
}

@keyframes maskAnim {
  0%, 1% { 
    clip-path: circle(0%);
    opacity: 1; 
  }
  100% { 
    clip-path: circle(100%); 
    opacity: 1; 
  }
}

注意: 1%キーフレームは、ブラウザがclip-pathの補間を要素がすでに持っている値ではなくcircle(0%)から確実に開始させるためのものです。これがないと、一部のブラウザは開始時に予期せずジャンプします。よりクリーンな代替案はanimation-fill-mode: bothを使用することです。これにより、アニメーション開始前に要素をfrom状態にロックします。

そこから、イラストレーション内の異なるSVGグループに同じアニメーションを適用します:

<g id="rainbow">...</g>
<g id="floor">...</g>
<g id="mushroom">...</g>
<g id="flowers">...</g>

どれだけサイケデリックでしょうか!?

スクロール駆動アニメーションは、アニメーションの進行を典型的なタイムラインではなくユーザーのスクロールに接続できるため優れています。

微妙でやや「トリッピー」な動き、例えば軽い視差効果に使用できます。この場合、ユーザーに近く見えるものが遠くにあるものより速く動くようにできます。

完全なCSSはこちらです:

#estrellas, #arcoiris, .text-line, #fecha, #arco, #flores, #dir, #piso, #barras {
  animation: moveUp both;
  animation-timeline: view();
}

@keyframes moveUp {
  from { transform: translateY(var(--offset)); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

#estrellas { --offset: 10vh; }
#arcoiris { --offset: 20vh; }
#fecha { --offset: 45vh; }
#arco { --offset: 50vh; }
#dir { --offset: 50vh; }
#flores { --offset: 65vh; }
#piso { --offset: 85vh; }
#barras { --offset: 90vh; }

animation-timeline: view()は、ユーザーがスクロールして要素がスクロールポートに入った瞬間にアニメーションを開始し、ビューからスクロールアウトしたときに完全に完了することを意味します。異なる速度で動かすために、テキスト分割で使用したように、インデックス付きの--offsetカスタムプロパティを使用して異なるオフセットに配置します。

3Dトランスフォーム

これはトリッキーで、パフォーマンスに注意する必要があります。Layoutitのようなツールは、CSS 3Dで完全に構築されたボクセル地形ジェネレーターを備えているため、負担を軽減できます。VoxCSSでさらに進化させることができます。これはCanvasやWebGLの複雑さなしに、CSS Gridレイヤーとトランスフォームのみを使用して3D立方体をレンダリングする完全なボクセルエンジンです。

スクロールと3Dエフェクトの組み合わせを組み合わせてみましょう。これはオプションAのキーワードリストにある「催眠的」と「ダンス」のアイデアをサポートするものです。これを見てください:

ここでは、perspectiveプロパティを使用して奥行きのあるシーンを設定し、シーン内のすべての子要素をtransform-style: preserve-3dで3D空間にラップしています。これにより、すべての子画像要素が奥行き軸(またはz軸)に沿って回転および平行移動します。

それをtransform: rotateYを使用するスクロール駆動アニメーションに接続してみましょう:

.scene {
  perspective: 1200px;
}

.img-wrapper { 
  transform-style: preserve-3d; 
  animation: rotateImg linear;
  animation-timeline: scroll();

  > img {
    transform: rotateY(270deg) translate3d(0, 50px, var(--distance));
  }

  > img:nth-child(2) {
    transform: rotateY(180deg) translate3d(0, 50px, var(--distance));
  }
}

/* etc. */

@keyframes rotateImg {
  to { transform: rotateY(360deg); }
}

カスタムカーソル

cursorは最も使われていないCSSプロパティの1つかもしれません。多くのカーソルタイプを使用できますが、どこまで進めるかについては意見があります

画像を使って遊び、ユーザーがホバーしたときに異なるコンテナに異なるカーソルを表示できます。私は個人的に透明度サポートのためにSVGとPNG画像を使用しますが、このプロパティは任意のラスター画像をサポートしています。

カーソルサイズはブラウザによって異なることに注意してください:Firefoxはカスタムカーソルを32×32pxに制限していますが、Chromeは128×128pxまでサポートしています。ほとんどのブラウザは、高DPI(Retina)画面で32×32pxより大きいカーソルを表示しないか、ダウンスケールします。カーソルを32×32pxに保つのが、一貫性を確保するための最も安全な選択です。

例:

.box1 {
  cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB0AAAAZCAMAAAD63NUrAAAACVBMVEX///8AAAD///9+749PAAAAAXRSTlMAQObYZgAAAFZJREFUeNqdzksKwDAIAFHH+x+6lIYOVPOhs5OHJnES/5UkYKEkU7xjijSIm50iFh4fAXgYDd/yumVVRSwsqq/nRA3xVK0oo06d5U6DpQZ7PV7lMxH7LkaQAbYFwryzAAAAAElFTkSuQmCC),auto; 
}

最も広いブラウザサポートを確保するために、複数のフォールバックを設定することもできます:

body {
  cursor: url('path-to-image.png'), url('path-to-image-2.svg'), url('path-to-image-3.jpeg'), auto;
}

これはすべてクールですが、デフォルトのWeb動作を変更するようなものについてはアクセシビリティを念頭に置く必要があります。カスタムカーソルは、ボード全体ではなく、非常に特定の要素に適用すると楽しいかもしれません。

ボーナス:アンカーポジショニング

締めくくる前に、もう1つ。 Kevin Powellのデモに触発されて、CSSアンカーポジショニングを試しています。各アイテムに疑似要素をアタッチするのではなく、現在ホバーされているアイテムに1つの疑似要素をアタッチするために使用できます。つまり、1つの要素を作成し、ホバーされた要素にアンカリングします。例えばカードをハイライト表示します:

それはカード間のホバーステートをトランジションできるなど、面白い可能性を開きます。このケースでは、Easing Wizardの助けを借りて、linear()関数を使用して自然なバウンスを実現しています。

結論

記憶に残るWeb体験を作成するための技術的な障壁は、今ではほとんどなくなりました。ここで取り上げたすべてが、追加のJavaScriptを完全に必要としないモダンCSS機能でどこまで行けるかのアイデアを与えてくれることを願っています。以前のような複雑な技術的オーバーヘッドなしに、これまで以上に多くの可能性があります。

したがって、これは可能か?と尋ねる代わりに、最も重要な質問は、この動きはより良いストーリーを語るか?になります。はいなら、採用してください。これらのツールを「できるから」ではなく、より良いストーリーを語るのに役立つから、そしてそれがアクセシブルでパフォーマンスが高いから使用してください。

そしてもちろん、ここにあるものはそれを行う方法のほんの一部です。しかし、あなたの仕事でどのような記憶に残る体験を使ったことがありますか?または他のサイトで見たことはありますか?