💡

iOS ChromeでのCSS transitionチラつきを直した

2024/12/04に公開

はじめに

PCのlocalhostでは問題なく動いていたtransitionアニメーションが、iOS Chromeではスクロールのたびにチラつきが生じたため、修正した記録です。

アニメーション内容

スクロール位置によって、「名前のみ」「アイコン+名前」のオブジェクトをふわっと入れ替えるアニメーションを作っていました。

最初の実装

現在作っているサイトは簡単なアニメーションばかりなので、ライブラリを入れるまでもないと思い、すべてCSSの transition または animation で実装していました。

今回は transitionopacity の変化に対してアニメーションさせました。
(記事用に書き直したコードのため、間違いある可能性があります)

page.module.css
// 「名前のみ」のコンポーネントの初期状態
.defaultName {
  transition: opacity 0.3s;
  opacity: 1;
}

// 規定のスクロール位置に達したら、このスタイルを付与する
.hideDefaultName {
  opacity: 0;
}

// 「アイコン+名前」のコンポーネントの初期状態
.stickyName {
  position: absolute;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  transition: opacity 0.3s;
  opacity: 0;
}

// 規定のスクロール位置に達したら、このスタイルを付与する
.showStickyName {
  opacity: 1;
}

問題点

冒頭に記載した通り、iOSのChromeで確認するとスクロールのたびに2つのオブジェクトがチラチラと入れ替わる現象に見舞われました。

どこに問題があるかわからなかったため、以下の3パターンを考えて調査を始めました。

  • ブラウザ依存のスタイルを記述する必要があるかも(--webkit〜のような)
  • アニメーションライブラリを使ったら内部でいい感じに対処してくれて直るかも
  • スクロールの検知に問題があるかも

試したこと

ブラウザ依存のスタイルを記述

調査中、以下の記事で同様の事象に対処していそうだったので、記載の通りにスタイルを追加してみました。

http://skyguild.jp/2017/10/safari_transition/

結果、特に変化はありませんでした。
iOS Chrome以外にも問題の起きるOS/ブラウザはあるかもしれないため、ひとつひとつにこのような対処をするのも骨が折れると思い、この線で続けるのはやめました。

アニメーションライブラリを使う

ライブラリならなんやかんや対処もらえるかなと思い、framer motion を使うことにしました。
検索して出た記事を参考に yarn add framer-motion してライブラリ追加しましたが、公式サイトを見ると motion になっているようで、removeしてNext.js 15に対応する公式サイトの指示通りに入れ直しました。

以下のように、スクロール位置によって変化させたステータスで、「名前のみ」「アイコン+名前」のコンポーネントを出し分けました。

pages.tsx
// 一応「Aを非表示→Bを表示」の順序で動くように、mode='wait'を設定
<AnimatePresence mode="wait">
    {showSticky ? (
        <motion.div
          key="sticky"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{
            duration: 0.2,
          }}
        >
          // 「アイコン+名前」のコンポーネント
          <div className={styles.stickyName}>
            ...
          </div>
        </motion.div>
        ) : (
        <motion.div
          key="default"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{
            duration: 0.2,
          }}
        >
          // 「名前のみ」のコンポーネント
          <div className={styles.defaultName}>
            ...
          </div>
        </motion.div>
    )}
</AnimatePresence>

localhostでは問題なく、むしろCSSで実装していた時よりも美しくアニメーションしてくれましたが、まだiOSではチラつきが発生しました。

スクロールの検知方法を変更(これで解決)

これまでのコードでは、スクロール検知を以下のように addEventListener() で行っていました。

pages.tsx
  const onScroll = useCallback(() => {
    // 「名前のみ」「アイコン+名前」のコンポーネントをくくってnameRefに紐付け
    if (!nameRef?.current) return;
    const top = nameRef?.current.getBoundingClientRect().top;
    if (top === 0) {
      setShowSticky(true);
    } else {
      setShowSticky(false);
    }
  }, []);
  useEffect(() => {
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, [onScroll]);

motion でもスクロール位置の検知が可能だったようなので、以下のように取得してからstateにセットしてみました。

pages.tsx
const { scrollY } = useScroll();

useMotionValueEvent(scrollY, "change", (latest) => {
    if (latest > 157) {
      setShowSticky(true);
    } else {
      setShowSticky(false);
    }
});

ここでチラつきが直りました!

おわりに

何の相性が悪かったのか結局定かにはなりませんでしたが、取り急ぎ直ってよかったです。
ライブラリを使いすぎてもライブラリの更新停止やバージョン兼ね合いなどで困ることがありますが、あまり時間のかけられない時には恩恵の方が大きいので、今後もうまく付き合っていきたいと思います。

Discussion