SCSSのMixinでホバーアニメーションを一元管理する方法

2023/02/22に公開

前置き

複数人である程度規模があり、コンポーネントの数もそれなりにあるサイト構築しているとどうしても、

  • 同じようなコンポーネントなのに使用箇所によってホバーアニメーションの印象が違う。
  • ホバーアニメーションのcssの記述方法や記述箇所にもばらつきが生じる。
  • アニメーションの変更があった場合に一括でできるようにしたい。

のような課題・要望が頻出。

また、上記の対応としてコンポーネント自体にホバーアニメーションを適用する場合、

  • アニメーションのスタイルはその要素自体のhover状態に準ずるスタイルと、親要素のhover状態に準ずるスタイル両方書かないといけないことや
  • ↑をした場合に、複合コンポーネント内でホバーアニメーションのスタイルが適用されたコンポーネントが含まれているが、アニメーションさせたくない時にキャンセルのスタイル書かないといけない。

などの問題も重なり、チーム内で明確な管理方法がないまま開発が進むと良くないな思ってました。
(開発環境内ではhoverした際のアニメーションを定義したmixinを利用していたのですが、コンポーネントごとに適用可否が変わるため、有用性が少なかったです。。。)

そこでtransformやopacityなどのアニメーションを構成するモーション単位でホバーの挙動を管理する方法を考えてみました。ちょっと長いですが、作成したコードの解説と、デザイン分解〜アニメーション実装の流れとに分けて紹介します。

作ったもの

ソースコード解説

ディレクトリ構成

ファイル 役割 格納されているもの(mixnとか関数とか)
index.html --- ---
hover.scss ホバーモーションを適用するために必要なmixnとか関数とかを定義。 ・transition (mixin):hover.scssファイルの外部でtransitionを設定するためのmixin
・base-transition (mixin):transitionを設定するためのmixin
・getTransitionProperties (function):モーションごとにtransitionプロパティを定義・モーション名に紐づけて呼び出す関数。
・hover-〇〇 (mixin):単一モーションのスタイルを定義
・hover (mixin):外部ファイルで呼び出すmixin・エントリポイント
main.scss hover.scssで定義したホバーモーションを呼び出してHTML要素に適用。

主な機能とできること

  • モーション単位でアニメーションするCSSプロパティの値とCSSトランジションを設定可能にする。
  • 以下の場合に対して単一のmixinの呼び出しでアニメーションの適用を可能にする。
    • ホバーする要素自体に単一・複数のモーションを適用
    • ホバーする要素配下の子要素に単一・複数のモーションを適用
    • ホバーする要素配下の複数の子要素毎に単一・複数のモーションを適用

ポイント

使用コンポーネントが限定されて、汎用性が下がるため以下のことに気をつけたほうが良いです。

  • hover.scssに特定のコンポーネントを指定するセレクタを記述しない。
  • 定義するアニメーションは1モーション(1cssプロパティ)ごとに分解する。

詳細説明

今回の肝となるファイルはhover.scssなのでこちらをメインで解説します。
※文章にすると長くなるのでポイント箇条書きにします。

hover (mixin):外部ファイルで呼び出すmixin・エントリポイント

@mixin hover($properties: null, $options: null) {
  @each $property in $properties {
    $c: null;
    $v: null;
    @if $options != null {
      @each $key, $option in $options {
        @if $key == $property {
          $c: if(map-get($option, 'child'), map-get($option, 'child'), null);
          $v: if(map-get($option, 'value'), map-get($option, 'value'), null);
        }
      }
    }

    // PCレイアウトにのみスタイルを反映したいモーション
    @include media-pc {
      @if $property == 'color' {
        @include hover-color($child: $c, $value: $v);
      }
      @if $property == 'scale' {
        @include hover-scale($child: $c, $value: $v);
      }
      @if $property == 'horizontalIn' {
        @include hover-horizontalIn($child: $c, $value: $v);
      }
    }

    // transition 設定
    @if $c != null {
      @include base-transition($properties, $child: $c);
    } @else {
      @include base-transition($properties);
    }
  }
  • main.scssにてホバー対象の要素(基本的にはaタグ)のセレクタを指定してmixinを呼び出す。
  • 引数にリスト型・マップ型の変数を指定可能にする。第1引数には適用したいモーションの名前、第2引数には必須ではないが、モーションの適用対象がホバー要素の子要素の場合はCSSセレクタや、アニメーション後のCSSプロパティの値を指定する。
  • 第1引数で渡されたリスト内でeachを回して、その中で必要なモーション(hover-〇〇)mixinを呼び出し。
  • ↑と同様に必要なトランジション設定(base-transition)mixinを呼び出す。

hover-〇〇 (mixin):単一モーションのスタイルを定義

ここではカラー変更のアニメーションを例とします。

/// @group motion
/// カラー変更
@mixin hover-color($args...) {
  $option: meta.keywords($args);
  $child: if(map-get($option, 'child'), map-get($option, 'child'), null);
  $value: if(
    map-get($option, 'value'),
    map-get($option, 'value'),
    $color-hover
  );
  @if $child != null {
    &:hover {
      #{$child} {
        color: $value;
      }
    }
  } @else {
    &:hover {
      color: $value;
    }
  }
}
  • 引数を可変長引数にし、meta.keywords関数を利用して、モーション定義に必要な変数を取得する。
  • 子要素のセレクタ指定がある場合やプロパティの値が指定されている場合も含めて、アニメーション前後のスタイルを設定する。

base-transition (mixin):transitionを設定するためのmixin

/// @group transition
/// @param {Lists} $properties - transitionを設定するアニメーションのリスト
/// hover.scssファイル内の"hover-〇〇 mixin"から呼び出す場合のトランジション
@mixin base-transition($properties: (), $args...) {
  $option: meta.keywords($args);
  $child: if(map-get($option, 'child'), map-get($option, 'child'), null);
  $transiton-in: map-get(
    getTransitionProperties($properties),
    in
  ); // トランジションイン用のプロパティの値
  $transiton-out: map-get(
    getTransitionProperties($properties),
    out
  ); // トランジションアウト用のプロパティの値
  @if $child != null {
    #{$child} {
      transition: #{$transiton-out};
    }
    &:hover {
      #{$child} {
        transition: #{$transiton-in};
      }
    }
  } @else {
    transition: #{$transiton-out};
    &:hover {
      transition: #{$transiton-in};
    }
  }
}
  • hover-〇〇mixinと作り方は一緒。子要素のセレクタ指定がある場合やプロパティの値が指定されている場合も含めて、アニメーション前後のトランジションを設定する。

getTransitionProperties (function):モーションごとにtransitionプロパティを定義・モーション名に紐づけて呼び出す関数。

/// @group transition
/// @param {Lists} $properties - transitionを設定するアニメーションのリスト
/// モーションごとのtransition設定を取得する関数
@function getTransitionProperties($properties: ()) {
  $transiton-in: ''; // トランジションイン用のプロパティの値
  $transiton-out: ''; // トランジションアウト用のプロパティの値
  // 指定されたhoverモーションの数だけtransitionの設定をしないといけないので@eachで回す。
  @each $property in $properties {
    @if $transiton-in != '' {
      $transiton-in: $transiton-in + ', ';
    }
    @if $transiton-out != '' {
      $transiton-out: $transiton-out + ', ';
    }
    // スケールのトランジション
    @else if $property == 'scale' {
      $transiton-in: $transiton-in + 'transform 0.3s #{$ease}';
      $transiton-out: $transiton-out + 'transform 0.6s #{$ease}';
    }
    // カラーのトランジション
    @if $property == 'color' {
      $transiton-in: $transiton-in + 'color 0.3s #{$ease}';
      $transiton-out: $transiton-out + 'color 0.6s #{$ease}';
    }
    // 水平移動のトランジション
    @else if $property == 'horizontalIn' {
      $transiton-in: $transiton-in + 'transform 0.3s #{$ease}';
      $transiton-out: $transiton-out + 'transform 0.6s #{$ease}';
    }
    // all
    @if $property == 'all' {
      $transiton-in: $transiton-in + 'all 0.3s #{$ease}';
      $transiton-out: $transiton-out + 'all 0.6s #{$ease}';
    }
  }
  @return (in: $transiton-in, out: $transiton-out);
}
  • 作成するモーションのトランジションプロパティを定義する。
  • 引数(リスト)で指定されたモーションの数だけ、transitionプロパティをカンマ区切りで複数設定してアニメーション前後のトランジション設定をマップ型でリターンする。

呼び出す時

// ホバー要素に単一モーションを適用する場合
@include h.hover( 'color' ); 

// ホバー要素の子要素に対して複数モーションを適用する場合
@include h.hover(
      ('horizontalIn', 'color'), // 適用したいモーションの名前を指定
      ( 
        horizontalIn: ( 
          child: '.text.-horizontalin' // 適用したい対象要素を指定
        ),
        color: ( 
          child: '.text.-color', // 適用したい対象要素を指定
          value: green // アニメーション後の値を指定
        )
      )
    ); 
  • mixinの呼び出しはhover mixinのみ。
  • 必要な場合は子要素とアニメーション後の値も指定する





モーションベースでスタイルの付け方を考えてみる。

上記で作成したmixinに新しいモーションをつける場合は、以下のような流れでデザインを分解〜実装します。

例えばこのUIはどんなモーションから構成されているか?

※左がホバー前、右がホバーした状態です。

1. ボタンを構成する要素・モーション・共通化できるもの、などがいくつあるかの視点で要素を分解してみる。

  • 変化する要素がいくつかるか?
    • 背景
    • 枠線
    • テキスト
    • 矢印(アイコンフォントを想定)
  • それぞれどんなモーションをするか?
    • 背景:左からボタンと同じ形の背景要素が水平移動する。
    • 枠線:色がグレーから青色へフェードする。
    • テキスト:色が黒から白へフェードする
    • 矢印:色が青から白へフェードする。
  • 共通するアニメーション・スタイル・プロパティがあるか?
    • テキストと矢印は一緒のスタイルでモーション適用できそう。(colorプロパティのアニメーション)

2. 分解した要素から作らなければならないモーションの数を考えてみる。

  • 水平移動
  • 色(colorプロパティ)
  • 枠線の色(border-colorプロパティ)

→3つmixinつければいけそうじゃん!

3. 余裕があればtransitionの設定も共通化できるか考えてみる。

  • 今回は色(colorプロパティ)と枠線の色(border-colorプロパティ)のtransitionは一緒でよさそう。※HTML要素自体・その役割・CSSプロパティカテゴリが一緒だから共通化できるとは限らない。

最終的にできたもの

上記を踏まえての、作成例です。

まとめ

メリットとしては冗長になりがち、読み解きが必要な場合が多いhoverモーションをmixin一つで呼び出しができたり、mixinの中でcssセレクタを引数に使うことでhoverアニメーションを適用したい要素が見やすくなるとかかなと思います。少し記述に慣れが必要ですが、感覚的にはgsapでcssプロパティをアニメーションする際の記述に似てるかなと。また、コンポーネント自体が多かったり、複合コンポーネントが多い場合に便利かなと感じています。

課題点としては、hover mixinからトランジションの設定はできないため、個別でトランジションの設定を変えたいときなどですかね。そのままcss書くか、別のmixinを作るかなどの工夫が必要です。例えばモーション間でスタートのタイミングを変えるためdelayをかけたい場合とか、イージングを変えたい時とか。

サイトによって機能追加したり、逆に減らしたりして使いやすく改造するとベターだと思います。

以上!

Discussion