Gemcook Tech Blog
📖

data属性で実現する表示・非表示アニメーションの作り方

2024/12/02に公開

はじめに

UI・UX向上のためにコンテンツを表示・非表示する際にアニメーションをつける。というのは良くある実装だと思います。しかし、実現する為の実装方法は一つではなく、どの方法が良いのか分からずに毎回モヤモヤが残る実装になっていました。

data属性を使うことで、JavaScriptとCSSで責務を分けたシンプルな表示・非表示のCSSアニメーションが実装できたので紹介したいと思います。

作成するもの

ボタンをクリックすると、アニメーション付きで表示・非表示になる <Card /> コンポーネントを持つページを作成していきます。

data属性とは

data属性とは特定の要素に関連付ける必要があるが、定義する必要のないデータを表現する為の属性です。これにより、標準外の属性やDOMの追加プロパティなどの特殊な方法に頼らず、HTML要素に追加情報を格納することができます。

https://developer.mozilla.org/ja/docs/Learn/HTML/Howto/Use_data_attributes

HTMLの構文

HTMLに名前が data-* で始まる属性を使用することで、要素にカスタムデータを格納することができます。これにより視覚的に影響を与えることなく、特定の情報を埋め込むことができます。

<div
  data-theme-color="purple"
  data-artist="jimi-hendrix"
/>

JavaScriptからのアクセス

HTMLで作成されたdata属性は、JavaScriptのdatasetプロパティを使用することで、data属性を取得・編集をすることができます。
data-* の後の属性名はキャメルケースに変換されることに注意してください。

const div = document.querySelector("#div");

div.dataset.themeColor; // "purple"
div.dataset.artist; // "jimiHendrix"

// data属性を追加する場合
div.dataset.age = 27;

CSSからのアクセス

属性セレクタを利用する事で、特定の条件を満たす要素にスタイルを適用できます。また、attr() 関数を使用してコンテンツを生成すること、:notを使い属性がないということを表現することもできます。

div[data-theme-color=purple] {
  background-color: purple;
}
div::before {
  content: "好きなアーティストは " attr(data-artist) " です。";
}
div:not([data-artist])::before {
  content: "好きなアーティストが定義されていません。";
}

実装方法

いまから二つの実装方法をみていきたいと思います。前者はdata属性を使わない実装例で、後者は同じ挙動ををdata属性を使い実装したものです。

data属性を使わない実装

Cardの開閉状態はisOpenというuseStateで管理され、useEffectを使ってisOpenの変化に応じて、アニメーションクラスを動的に切り替えます。クラスの切り替えによって、表示時にはfade-in、非表示時にはfade-outのスタイルを適用させています。

Page.tsx
const Page = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [initialState, setInitialState] = useState(true);
  const [animationStyle, setAnimationStyle] = useState<SerializedStyles>();

 useEffect(() => {
    if (isInitialState) return setIsInitialState(false);

    if (isOpen) {
      setAnimationStyle(styles.fadeIn);
    } else {
      setAnimationStyle(styles.fadeOut);
    }
  }, [isOpen]);

  return (
    <>
        <div className={`${styles.card} ${animationClass}`}>
          <Card />
        </div>
        <Open onClick={() => setIsOpen(true)}/>
        <Close onClick={() => setIsOpen(false)}/>
    </>
  );
};
styles.ts
const styles = {
  card: css({
    ・・・
    visibility: "hidden",
  }),
  fadeIn: css({
    animation: fadeIn,
    visibility: "visible",
  }),
  fadeOut: css({
    animation: fadeOut,
    visibility: "visible",
  }),
}

data属性を使った実装

Cardの状態はvisibleStatusで管理しており、その値をdata属性(data-visibleStatus)に入れるだけです。CSSの属性セレクタと組み合わせることで、Page側で一切スタイルを操作することなくアニメーションを可能にしています。

Page.tsx
type VisibleStatus = "initialState" | "open" | "close"

const Page = () => {
  const [visibleStatus, setVisibleStatus] = useState<VisibleStatus>("initialState");

  return (
    <>
        <div data-visibleStatus={visibleStatus} className={styles.card}>
            <Card />
        </div>
        <Open onClick={() => setVisibleStatus("open")}/>
        <Close onClick={() => setVisibleStatus("close")}/>
    </>
  );
};
styles.ts
export const styles = {
  card: css({
    ・・・
    visibility: "visible",
    "&[data-visibleStatus=initialState]": {
      visibility: "hidden",
    },
    "&[data-visibleStatus=open]": {
      animation: fadeIn
    },
    "&[data-visibleStatus=close]": {
      animation: fadeOut
    },
  }),
};

実装まとめ

useEffectを使用した実装例では、Page側で状態管理に加えてアニメーションクラスの切り替えを操作・管理する必要がありましたが、data属性を使用することで、CSS側で自動的にスタイルを当ててくれます。つまりdata属性を使う事で、Page側では状態管理、CSS側ではアニメーションの定義といったように、それぞれの責務を明確に分離することができます。

もっと短く書きたい方へ

data属性を使った実装例ではuseEffectで使用したisOpenのようなboolean型を使いませんでしたが、実は以下のようにboolean型を使って実装することもできます。

実装例

Page.tsx
const Page = () => {
  const [isOpen, setIsOpen] = useState<boolean>(); // 初期値がundeifned

  return (
    <>
        <div data-isOpen={isOpen} className={styles.card}>
            <Card handleClose={() => setIsOpen(false)} />
        </div>
        <Open onClick={() => setIsOpen(true)}/>
        <Close onClick={() => setIsOpen(false)}/>
    </>
  );
};
styles.ts
export const styles = {
  card: css({
    ・・・
    visibility: "hidden",
    "&[data-isOpen]": {
      visibility: "visible",
    },
    "&[data-isOpen=true]": {
      animation: fadeIn,
      pointerEvents: "all",
    },
    "&[data-isOpen=false]": {
      animation: fadeOut,
      pointerEvents: "none",
    },
  }),
};

解説

初期値をundeifnedとすることで、初回のレンダリングの際にdata-isOpenが無いという状態を作り出すことができるので、data属性を使った実装 と同じ挙動の実装をすることができます。

ただ !isOpen ? のような条件を使う場合、false | undefined が入ってくるのでバグやエラーに注意して使う必要があります。

さいごに

data属性は非常に便利ではありますが、アニメーションで使うというのは、参考例が少なく慎重に使う必要がありますが、今回のような簡単な実装であれば、十分に使えると思います。
フロントエンドの動きは早いので、今回のように新しい実装方法がないか、最新動向をチェックしていきたいと思います。

Gemcook Tech Blog
Gemcook Tech Blog

Discussion