【Sass】PCのみHover効果を有効にするmixinを紹介!モバイルもHover効果の有無を選択可能

今回は超久しぶりのweb系の記事になります。

今回の記事は、Sassのmixinでホバー効果を制御するというものです。

目次

PCではHover効果有効、モバイル端末ではHover効果無効のmixin

上記の通りなのですが、

PCとモバイルのHover制御は面倒ですよね?PCのHoverスタイルをモバイルで打ち消したりなど、そう面倒だと思いこのmixinを作成しました。

機能は以下の通りです。

  • PCはHover効果有効
  • モバイル端末はHover効果無効(有効に変更可)
  • キーボード操作でフォーカスした場合はHover効果有効

ソースコード(_hover.scss)

長いですが以下のソースコードをコピペして任意の名前で保存してください。

//指定したclassがネスト内にあるかチェック
@function is-inside($target) {
  @return if("#{selector-replace(&, $target, ".replaced")}" != "#{&}", true, false);
}

//スマホではホバー効果を適用しないhover mixin
//フォーカス時にもホバー効果を適用
@mixin hover ($target: null, $mobile: false, $focus: null) {

  $focusTarget: if($focus == null, $target, $focus);

  @if $target == null {
    @media (hover: hover) {
      &:hover {
        @content
      }
    }
    //ie用(事前にjsでユーザーエージェントでbodyにclass付与)
    @at-root [data-ua-browser="ie"] &:hover {
      @content;
    }
    //キーボードでフォーカスしたとき(主にtabキー移動)
    //safariは「環境設定」の「詳細」タブで操作中の項目を強調表示にチェック
    &[data-focus-visible-added]:focus {
      @at-root [data-js-focus-visible] & {
        @content
      }
    }
    //スマホでホバー有効設定の場合
    @if $mobile == true {
      @media (hover: none) {
        &:active {
          @content
        }
      }
    }
  } @else {
    //ネストの外か内を判定
    @if is-inside($target) {
      //ネストの内側
      @media (hover: hover) {
        @at-root #{selector-replace(&, $target, $target + ":hover")} {
          @content
        }
      }
      //ie用(事前にjsでユーザーエージェントでbodyにclass付与)
      @at-root #{selector-replace(&, $target, $target + ":hover")} {
        @at-root [data-ua-browser="ie"] & {
          @content;
        }
      }
      //キーボードでフォーカスしたとき(主にtabキー移動)
      @if is-inside($focusTarget) {
        @at-root #{selector-replace(&, $focusTarget, $focusTarget + "[data-focus-visible-added]:focus")} {
          @at-root [data-js-focus-visible] & {
            @content
          }
        }
      } @else {
        @at-root #{"[data-js-focus-visible] " + $target + "[data-focus-visible-added]:focus " + &} {
          @content
        }
      }

      //スマホでホバー有効設定の場合
      @if $mobile == true {
        @media (hover: none) {
          @at-root #{selector-replace(&, $target, $target + ":active")} {
            @content
          }
        }
      }
    } @else {
      //ネストの外側
      @media (hover: hover) {
        @at-root #{$target + ":hover" + " " + &} {
          @content
        }
      }
      //ie用(事前にjsでユーザーエージェントでbodyにclass付与)
      @at-root #{$target + ":hover" + " " + &} {
        @at-root [data-ua-browser="ie"] & {
          @content
        }
      }
      //キーボードでフォーカスしたとき(主にtabキー移動)
      @if is-inside($focusTarget) {
        @at-root #{selector-replace(&, $focusTarget, $focusTarget + "[data-focus-visible-added]:focus")} {
          @at-root [data-js-focus-visible] & {
            @content
          }
        }
      } @else {
        @at-root #{"[data-js-focus-visible] " + $target + "[data-focus-visible-added]:focus " + &} {
          @content
        }
      }
      //スマホでホバー有効設定の場合
      @if $mobile == true {
        @media (hover: none) {
          @at-root #{$target + ":active" + " " + &} {
            @content
          }
        }
      }
    }
  }
}

事前準備

このmixinを使用するには事前準備が必要になります。

以下項目の2, 3は必須ではありませんがフォーカス時とIEでうまく動作しません。
IEを無視できる場合は3を飛ばしても問題ありません。

1. 読み込み

まずはmixinの読み込みですね。
ここでは今回のmixinのファイルはfoundation/mixin/ディレクトリに_hover.scssというファイルで設置したとします。

node-sass(LibSass)場合

@import "foundation/mixin/hover";

sass(Dart Sass)の場合

DartSassの場合は@forwardと組み合わせたりするかと思いますが、ここではとりあえず以下のように読み込みます。

@use "foundation/mixin/hover" as *;

2. focus-visible.jsの読み込み

focus-visible.jsとはcssの疑似クラスである:focus-visibleのポリフィルです。
今回のmixinではキーボード操作でフォーカスした場合の処理に使用しています。

npmの場合

インストール

npm install --save focus-visible

読み込み

import focusVisible from "focus-visible";

CDNの場合

<script src="https://cdn.jsdelivr.net/npm/focus-visible@5.2.0/dist/focus-visible.min.js"></script>

CSSに以下を追加

以下のcssを自身が管理しているcssに追加します。
これでキーボードの操作以外でフォーカスされたときにoutlineを削除します。

.js-focus-visible :focus:not(.focus-visible) {
    outline: 0;
}

3. IEをユーザーエージェントで判別しておく

このhover mixinはそのままではIEに非対応なのでユーザーエージェントで事前に判別しておく必要があります。

このmixinはブラウザがIE場合、bodyにdata-ua-browser="ie"を付与する必要があります。
以下のjsはIEの場合bodyにdata-ua-browser="ie"を付与するjsです。

const userAgent = window.navigator.userAgent.toLowerCase();

if(userAgent.indexOf('msie') != -1 || userAgent.indexOf('trident') != -1) {
  //ブラウザがIEだったらbodyに「data-ua-browser="ie"」を付与する
  document.body.setAttribute('data-ua-browser', 'ie')
}

ユーザーエージェントでIE判別参考記事 : https://arts-factory.net/useragentbrowser/

hover mixinの使い方

まず、ここでの使い方は以下のHTMLを例として使用します。

<button class="c-button">
  <span class="c-button__text">ボタンテキスト</span>
</button>

基本の使い方

.c-button {
  @include hover() {
    opacity: 0.8;
  }
}

コンパイル後

@media (hover: hover) {
  .c-button:hover {
    opacity: 0.8;
  }
}
[data-ua-browser=ie] .c-button:hover {
  opacity: 0.8;
}

[data-js-focus-visible] .c-button[data-focus-visible-added]:focus {
  opacity: 0.8;
}

特定の要素がHoverしたとき

第一引数にセレクターを指定することでその要素がHoverしたときのHover効果を設定できます。

※HTMLが親子関係にある必要があります。

.c-button__text {
  @include hover(".c-button") {
    color: red;
  }
}

コンパイル後

@media (hover: hover) {
  .c-button:hover .c-button__text {
    color: red;
  }
}
[data-ua-browser=ie] .c-button:hover .c-button__text {
  color: red;
}

[data-js-focus-visible] .c-button[data-focus-visible-added]:focus .c-button__text {
  color: red;
}

モバイルでもHoverを有効にしたい場合

第二引数をtrueもしくは$mobile: trueのように直接指定してtrueにすることでモバイルでもHoverが有効になります。

※Androidだとホバーを無効にしても:focusが反応してしまうので、スマホのホバーは:activeで対応しています。

.c-button {
  @include hover($mobile: true) {
    opacity: 0.8;
  }
}

.c-button__text {
  @include hover(".c-button", true) {
    color: red;
  }
}

コンパイル後

@media (hover: hover) {
  .c-button:hover {
    opacity: 0.8;
  }
}
[data-ua-browser=ie] .c-button:hover {
  opacity: 0.8;
}

[data-js-focus-visible] .c-button[data-focus-visible-added]:focus {
  opacity: 0.8;
}

@media (hover: none) {
  .c-button:active {
    opacity: 0.8;
  }
}

@media (hover: hover) {
  .c-button:hover .c-button__text {
    color: red;
  }
}
[data-ua-browser=ie] .c-button:hover .c-button__text {
  color: red;
}

[data-js-focus-visible] .c-button[data-focus-visible-added]:focus .c-button__text {
  color: red;
}

@media (hover: none) {
  .c-button:active .c-button__text {
    color: red;
  }
}

第一引数のセレクターがaタグ、buttonタグではない場合

基本的にHover効果はaタグ、buttonタグに使用するものですが、第一引数のセレクターがそれ以外の場合はフォーカス時にHover効果が有効になりません。

その場合は第三引数にセレクターを指定するか、直接$focus: ".box__text"のようにセレクターを指定します。

※正直の機能はほぼ使わないです

ここではHTMLの例を以下とします。

<div class="box">
  <a href="#" class="box__text">
    テキスト
  </a>
</div>

$focusに指定するセレクターはHover効果を適用したいaタグもしくはbuttonタグを指定します。

.box {
  .box__text {
    @include hover(".box", $focus: ".box__text") {
      color: blue;
    }
  }
}

コンパイル後

@media (hover: hover) {
  .box:hover .box__text {
    color: blue;
  }
}
[data-ua-browser=ie] .box:hover .box__text {
  color: blue;
}

[data-js-focus-visible] .box .box__text[data-focus-visible-added]:focus {
  color: blue;
}

関連記事

今回のmixinは以前投稿した以下の記事の技術を一部使用しています。

まとめ

今回はhover mixinを紹介してみました。

実際に僕はこのmixinは2年ほど前にベースを作成しアップデートし続けて今の形になりました。

当ブログのHover効果もこのmixinを使用しています。
当ブログはNext.jsで動作していますが、そのCSSはCSS Modulesを採用しています。CSS Modulesでもこのmixinは問題なく使用できております。

この記事を読んで興味を持たれた方は使用してみてはいかがでしょうか。

それにしてもブログのCSSとHTML構造をリファクタしたい...デザインも変えたい...しかしそれをやるくらいなら記事書いた方良い気がする...
余談でした(笑)