コハム

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

アニメーションCSSをもっと簡単に!@starting-styleとtransition-behaviorの実践テクニック

Using @starting-style and transition-behavior for enter and exit stage effects

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

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


@starting-styleを使って遊んでいたら、transition-behaviorと組み合わせることで、display: noneの切り替えや初回レンダリングのための純粋なCSSトランジション戦略を完成させるのに役立つかもしれないと思いました。

ダイアログとポップオーバーは、どちらもブラウザによってdisplayが切り替えられます。

中断可能なトランジション > キーフレームアニメーション

これから説明する内容の完全なデモをCodepenで試してみてください:

結果は?

  • display: noneを使用した要素の表示/非表示の共通スタイル
  • カスタムの出入り効果
  • 中断可能なモーション
  • 簡単なスタガリングの可能性
  • これはほぼ、トランジションを使用したDOM要素の統一的なオーケストレーションです。唯一の注意点は、何かをステージから外すためにdisplay: noneを使用する必要があることです。

Caniuse?

この投稿では新しい機能がいくつか使用されています。以下はそれらのcaniuseリンクです:

🧐 - @starting-style - transition-behavior -

BASELINE 2022 - :modal BASELINE 2022 - [popover] BASELINE 2024 - :popover-open BASELINE 2024 - @layer BASELINE 2022 - [hidden] OLD SCHOOL

ステージ入場

オリジナルの投稿のCSSとその数行の栄光はこちらです:

* {
  transition: opacity .5s ease-in;
  @starting-style { opacity: 0 }
}

クロスフェードの入場トランジションが発生します:

  • ページロード時
  • ページに挿入時
  • display: noneから戻る時

@starting-styleは、ステージ外の開始位置を描写しています。opacity: 0からステージ上の自然な休止位置(デフォルトではopacity: 1)に移行すると言っています。

入場と退場を区別しやすくするため、入場時は拡大した状態から始まり、休止位置に向かって縮小します。上から浮いてきたような感じです。

@starting-style { 
  opacity: 0; 
  scale: 1.1; 👈
}

scaleをトランジションに追加したので、補間されるようにtransition-propertyリストを更新する必要があります:

* {
  transition: 
    opacity .5s ease-in, 
    scale   .5s ease-in; 👈

  @starting-style { 
    opacity: 0; 
    scale: 1.1; 
  }
}

これで、要素がステージに入る際に縮小してフェードインするようになりました👍🏻

ただし、注意してください。もはや単なるクロスフェードではありません。

* {
  @media (prefers-reduced-motion: no-preference) { 👈
    transition: 
      opacity .5s ease-in, 
      scale   .5s ease-in;  
  }

  @starting-style { 
    opacity: 0; 
    scale: 1.1; 
  }
}

ステージ退場

ここでtransition-behaviorが登場します。

要素にdisplay: noneを設定すると、ブラウザがすぐにステージから要素を削除するため、トランジションが実行される時間がありません。transition-behavior: allow-discreteを追加すると、display: noneの変更を適用する前に経過すべき時間を指定できます。

* {
  @media (prefers-reduced-motion: no-preference) {
    transition: 
      opacity .5s ease-in, 
      scale   .5s ease-in,
      display .5s ease-in;               👈
    transition-behavior: allow-discrete; 👈
  }

  @starting-style { 
    opacity: 0; 
    scale: 1.1; 
  }
}

これで、ブラウザは要素がdisplay noneに設定されたときに即座に非表示にせず、.5s待ちます。これはトランジションの実行時間とちょうど同じです。すごいですね。

しかし、何か足りないものがあります...そうです、退場時のスタイルです!opacity、scale、displayをスタイリングできる状態をトリガーする何かが必要です。

私は、display: noneを設定するブラウザ提供の属性である[hidden]属性を利用することにしました!

* {
  @media (prefers-reduced-motion: no-preference) {
    transition: 
      opacity .5s ease-in, 
      scale   .5s ease-in,
      display .5s ease-in;
    transition-behavior: allow-discrete;
  }

  @starting-style { 
    opacity: 0; 
    scale: 1.1; 
  }

  &[hidden] { 👈
    opacity: 0;
    scale: .9;
    display: none !important; 
    transition-duration: .4s;
    transition-timing-function: ease-out;
  }
}

これで、要素に[hidden]属性が与えられたとき、ステージ外のスタイルを記述します。私は退場アニメーションを入場アニメーションよりも少し速くするのが良いと思うので、持続時間を.4sにしてease-out効果を加えました。

!importantが追加されていることに注意してください。ブラウザは属性に対してこれを自動的に提供しますが、その詳細度は弱いです。要素にカスタムのdisplayタイプが設定されている場合、noneは適用されず、効果が失われます。これは、独自のタイプのフラグをここで使用できることも意味します。

デモで「Toggle [hidden]」ボタンを使用すると、この効果を試すことができます。

ダイアログとポップオーバー

&[hidden]セレクタに追加することで、

と[popover]要素も他の要素と同じように退場できるようになります。

* {
  @media (prefers-reduced-motion: no-preference) {
    transition: 
      opacity .5s ease-in, 
      scale   .5s ease-in,
      display .5s ease-in;
    transition-behavior: allow-discrete;
  }

  @starting-style { 
    opacity: 0; 
    scale: 1.1; 
  }

  &[hidden],
  dialog:not(:modal),              👈
  &[popover]:not(:popover-open) {  👈
    opacity: 0;
    scale: .9;
    display: none !important; 
    transition-duration: .4s;
    transition-timing-function: ease-out;
  }
}

の場合、ダイアログが非表示になっているかどうかを知るために:modalの疑似クラスを使用しています。

[popover]の場合、ポップオーバーが非表示になっているかどうかを知るために:popover-openの疑似クラスを使用しています。

デモでダイアログやポップオーバーの表示ボタンを使用すると、この効果を試すことができます。

@layerのフレーバー

カスケードレイヤーを使用すると、匿名レイヤーを作成できます。私はこれを2つの部分からなる機能と考えており、これは*に広く適用されるこの入退場トランジションスタイルにうまく適合します。

  • オーバーライドが簡単であるべき
  • devtoolsでノイズが多すぎないようにすべき
@layer {}

これまでに作成したネストされたスタイルのセット全体をラップすることで、#1と#2を達成できます。なぜなら:

  • 降格された匿名レイヤーは、レイヤー化されていないスタイルよりも弱いか、作成者が弱いレイヤーであることを知っている別のレイヤーにインポートできる
  • レイヤーはスタイルペインで他のスタイルの下にスタイルをドロップする
@layer { 👈
  * {
    @media (prefers-reduced-motion: no-preference) {
      transition: 
        opacity .5s ease-in, 
        scale   .5s ease-in,
        display .5s ease-in;
      transition-behavior: allow-discrete;
    }

    @starting-style { 
      opacity: 0; 
      scale: 1.1; 
    }

    &[hidden],
    dialog:not(:modal), 
    &[popover]:not(:popover-open) { 
      opacity: 0;
      scale: .9;
      display: none !important; 
      transition-duration: .4s;
      transition-timing-function: ease-out;
    }
  }
}

私はこの効果が好きですし、カスケードレイヤーも好きです。しかし、必要なければこの@layerを使わなくても大丈夫です。

トランジション完了?

要素がdisplay noneにトランジションしたら、ノードをクリーンアップしたいですよね。

function onTransitionsEnded(node) {
  return Promise.allSettled(
    node.getAnimations().map(animation =>
      animation.finished))
}

使い方はかなり簡単です:

async () => {
  node.hidden = true
  await onTransitionsEnded(node)
  node.remove()
}

これでクリーンアップが簡単になりました。

デモの「remove」ボタンを使用すると、この効果を試すことができます。

結論

このCSSの一部は少し不思議に見えるかもしれませんが、最終的なスニペットに至るまでの道のりは理にかなっていると思います。

また、これをクラスなどにスコープを限定するのも理にかなっていると思います。おそらく、すべての要素に対してこの結果が望ましくないシナリオもあるでしょう。しかし、概念的にすべてをターゲットにするのは確かに楽しいです。

ビュートランジションは、これが解決する問題の一部に対して良いオプションですが、中断可能ではありません。また、ダイアログやポップオーバーとの統合も簡単ではありません。

トランジションは素晴らしく、ダイアログやポップオーバーの場合、ユーザーが効果を中断できるのは確かに価値があります。

この奇妙なCSSの考えに付き合ってくれてありがとうございます!

更新 #1:

の::backdropをトランジションさせたい場合は、いくつかのスタイルを追加する必要があります。また、目的によってはoverlayもトランジションさせる必要があるかもしれません。ダイアログとポップオーバーを完全にトランジションさせる、より実験的でないスニペットについては、このCodepenを参照し、そこからCSSを流用してください。

更新 #2:

すべてのトランジションが完了したことを知るための関数が更新されました。今では100%信頼性があり、getAnimations()を使用しています。テスト中にこれを試したところ、配列が空でした。しかし、後になって、それはトランジションが実行されていないからだとわかりました。トランジションが開始されると、getAnimations()は各トランジションと.finishedプロミスを返します。

©コハム