【GSAP】ホバーしたときに入れ替わるように下からテキストを表示させる方法【デモ付き】

りょーすけ
りょーすけ

リンクにホバーしたとき凄いお洒落な動きするものあるよね!?
どうやって実装するんだ~??

Vanilla JavaScript
Vanilla JavaScript

ホバー??私が手助けしましょう!!

GSAP MAN
GSAP MAN

僕も助太刀しますぞ!!

「ホバー」「お洒落な動き」この2つの単語が登場したらGSAPの出番です。

まずはどんな動きなのかデモを見ていただきましょう。

動きを確認 – デモ

各リンクにホバーするとテキストが入れ替わるように下から登場するのがわかると思います

See the Pen roll text with gsap by りょーすけ (@s_ryosuke_1242) on CodePen.

注目ポイント
  • 現在表示されているページのリンク(.is-active)は何も変化がない
  • リンクの有効範囲
  • 予めJSでテキストを一文字ずつspanタグに分割
  • 一文字ずつ動くタイミングをずらす

HTML

まずはHTMLから見てみましょう

<main class="container">
    <ul class="lists">
        <li class="list">
            <a class="js-splitText is-active" href="./">ホーム</a>
        </li>
        <li class="list">
            <a class="js-splitText" href="./">お知らせ</a>
        </li>
        <li class="list">
            <a class="js-splitText" href="./">商品情報</a>
        </li>
        <li class="list">
            <a class="js-splitText" href="./">ヒストリー</a>
        </li>
        <li class="list">
            <a class="js-splitText" href="./">ご予約</a>
        </li>
    </ul>
</main>

意外とスッキリしている印象ではないでしょうか。

デモの動きを見ているとホバーしたときに下から同じテキストが出てくるためaタグの中に同じテキストが入っていると予想しますが、SEOに影響をおよぼすことを考慮してHTML側で複製するのではなく、JavaScriptで同じテキストを生成しています

NGな例(個人的見解)

<li class="list">
  <a class="js-splitText is-active" href="./">ホーム</a>
  <a class="js-splitText is-active" href="./">ホーム</a>
</li>

OKな例

<li class="list">
  <a class="js-splitText is-active" href="./">ホーム</a>
</li>

is-activeは現在のページを示すスタイルを当てるために付与しているクラスです

CSS

次にCSSです

*{
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}
li{
    list-style: none;
}
a{
    text-decoration: none;
    color: inherit;
}
.container{
    width: 100%;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #333;
}
/*ここまでは見た目を整えるもの*/
.lists{
    display: flex;
    align-items: center;
}
.list{
    font-size: 18px;
    font-family: 'Noto Sans JP', sans-serif;
    color: white;
}
.list a{
    display: block;
    padding: 0 12px;
    overflow: hidden;/*はみ出た要素を隠す*/
    letter-spacing: .1em;/*文字間を広げて余裕のある感じに*/
}
.list a.is-active{
    color: red;
}
.list a:hover span{
    color: red;
}
.list a.is-active{
    cursor: initial;
}
/* display:flexとalign-items:centerは中身の高さに合わせるために記述*/
.text-wrap{
    position: relative;
    display: flex;
    align-items: center;
}
.after{
    position: absolute;
    top: 0;
    left: 0;
    display: flex;
    align-items: center;
}
.before{
    display: flex;
    align-items: center;
}
.before span,
.after span{
    line-height: 1.1;
    transition: color .3s ease-out;/*ease-outで余裕のある動きに*/
}
.after span{
    transform: translateY(100%);/*予め下から出現するテキストを一文字分下に配置しておく*/
}
注目ポイント

・display:flexで中身の高さを確保している点(absolute使うとheightが0になってしまうため)

・oveflow:hiddenではみ出たテキストを隠す

・.after(下から登場するテキスト)はposition:absolute;で要素を浮かせる→親要素(text-wrap)の高さが文字一つ分となる

・色変化をease-outにすることで余裕のある動き、品のある動きにしています

.beforeとか.afterとか.text-wrap、HTMLに記述されてないよね?と思ったはずです。

これはこのあと紹介するJavaScript内で要素を生成しています。

JavaScript

/* spanタグに分割 */
let splitTarget = document.querySelectorAll('.js-splitText');//ターゲットとなる要素を全取得
splitTarget.forEach((target) => {// target = ターゲット
    if(!target.classList.contains('is-active')){//ターゲットが'is-active'クラスを持っていない場合
        newText = '';//生成する要素を格納するための変数
        spanText = target.innerHTML;//ターゲットの中身を取得
        spanText.split('').forEach((char) => {// char = character 文字
            newText += '<span>' + char + '</span>';//一文字ずつspanタグで囲む
        });
        newTextBefore = "<div class='before'>"+newText+"</div>";//beforeクラスをつけた要素を生成
        newTextAfter = "<div class='after'>"+newText+"</div>";//afterクラスをつけた要素を生成
        newText = "<span class='text-wrap'>"+newTextBefore + newTextAfter+"</span>";//before after両要素を囲む要素生成
        target.innerHTML = newText;//ターゲットに生成した要素を挿入
    }
});

/* ターゲットにホバーした時の動き */
splitTarget.forEach((target)=>{
    if(!target.classList.contains('is-active')){//ターゲットが'is-active'クラスを持っていない場合
        let beforeSpan = target.querySelector('.before').querySelectorAll('span');//beforeの中にあるspanタグを全取得
        let afterSpan = target.querySelector('.after').querySelectorAll('span');//afterの中にあるspanタグを全取得
        target.addEventListener('mouseenter',()=>{//ホバーしたとき
            gsap.to(beforeSpan,{y:'-100%',stagger:.03,ease:"power2.out"})//0.03秒おきに各spanタグをy軸上に移動
            gsap.to(afterSpan,{y:'0%',stagger:.03,ease:"power2.out"})
        })
        target.addEventListener('mouseleave',()=>{//ホバーが外れたとき
            gsap.to(beforeSpan,{y:'0%',stagger:.03,ease:"power2.out"})
            gsap.to(afterSpan,{y:'100%',stagger:.03,ease:"power2.out"})
        })
    }
})

各行に説明を施しているのでぜひ理解の参考にしてみてください。

重要なポイント
  • ターゲットとなる要素に含まれるテキストは一文字ずつspanタグに分割→時間差でテキストが移動するアニメーションを付与するために
  • innerHTMLプロパティで要素を代入
  • テキストの動きはGSAPで補助
  • ease:”power2.out”で上品な動き
  • translateY(‘100%’)で予め一文字分移動している要素がtranslateY(‘0%’)となると元の位置に戻る動きをGSAPで処理

特に10行目から13行目の記述が重要

10行目、11行目で文字列の状態で要素を生成し12行目でnewText変数に代入

そして13行目のinnerHTMLでHTML要素としてターゲット要素を代入しています

まとめ

今回実装した内容はCSSでアニメーションが動く前の準備を整え、GSAPとJavaScriptの力を借りて実装するというイメージとなっております。内容が理解し辛い場合は一旦、JSの記述を消してHTML、CSSだけの状態にし検証ツールでどのような構造になっているかを見ると理解が捗ると思います。

最後までご覧いただきありがとうございました!またね~!