デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
右端の丸に、同じ形の SVG の矢印を 2 本重ねておきます。丸には overflow: hidden を付け、はみ出した矢印を切ります。カーソルが重なったら、1 本目を右へ押し出して丸の外へ抜き、少し遅れて 2 本目を左の外から中央へ入れます。2 本は同じ形なので、入れ替わったあとの見た目は始めと区別がつきません。
1 本目は 0.3 秒の power2.in で、矢印の幅の 1.7 倍だけ右へ送ります。2 本目は 0.18 秒遅れて動き出し、0.45 秒の power3.out で中央に止まります。加速して抜け、減速して入るので、1 本の矢印が右へ飛び出して、ひと回りして戻ってきたように見えます。遅れを 0.3 秒以上にすると丸が一瞬空になり、0.1 秒以下にすると 2 本が並んで見えます。
終わりの見た目が始めと同じなので、離れたときに逆再生する必要はありません。重なるたびに頭から流すだけです。ただし、入れ替わりの途中でカーソルが出入りしたときに頭から流し直すと、抜けかけた矢印が中央へ跳んで見えます。isActive() で動いている最中かを確かめ、その間は流し直さないようにします。
キーボードで移ってきたとき(focus)にも流します。入れ替わっても見た目は変わらないので、動きを減らす設定では何もしません。矢印を下向きにして縦に入れ替えればダウンロードのボタンに、斜め上向きにすれば外のサイトへのリンクに使えます。「詳しく見る」「次へ」のようなリンクのボタンや、カードの右下の矢印に向いています。
コード
<!-- 丸の中に同じ形の矢印を 2 本重ねる。1 本目が右へ抜け、2 本目が左から入る -->
<a class="arrow-button" href="/works/">
<span class="arrow-button__label">詳しく見る</span>
<span class="arrow-button__icon" aria-hidden="true">
<svg class="arrow-button__arrow" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6" /></svg>
<svg class="arrow-button__arrow" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6" /></svg>
</span>
</a>
.arrow-button {
--ink: #eeece6; /* ボタンの地と矢印の色 */
--ground: #111111; /* 文字と矢印の丸の色 */
display: inline-flex;
align-items: center;
gap: 16px;
height: 48px;
padding: 0 4px 0 24px;
border-radius: 999px;
background-color: var(--ink);
color: var(--ground);
font-size: 15px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.04em;
text-decoration: none;
white-space: nowrap;
}
/* 矢印の窓。はみ出した矢印は丸で切れる */
.arrow-button__icon {
position: relative;
flex: none;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--ground);
overflow: hidden;
}
/* 2 本とも丸の中央に置く。2 本目を左の外へ出しておくのは script の役目 */
.arrow-button__arrow {
position: absolute;
inset: 0;
width: 18px;
height: 18px;
margin: auto;
fill: none;
stroke: var(--ink);
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
}
.arrow-button:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 4px;
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".arrow-button").forEach((button) => {
// 入れ替わっても見た目は始めと同じなので、動きを減らす設定では何もしない
if (reduceMotion) return;
const [arrowOut, arrowIn] = button.querySelectorAll(".arrow-button__arrow");
// 1 本目が抜けきる少し前に 2 本目を入れ始めると、途切れずに入れ替わって見える
const swap = gsap
.timeline({ paused: true })
.fromTo(arrowOut, { xPercent: 0 }, { xPercent: 170, duration: 0.3, ease: "power2.in" }, 0)
.fromTo(arrowIn, { xPercent: -170 }, { xPercent: 0, duration: 0.45, ease: "power3.out" }, 0.18);
// 終わりの見た目が始めと同じなので、離れたときに戻す動きは要らない。
// 入れ替わりの途中で入り直したときは流し直さない(頭から流すと、抜けかけた矢印が中央へ跳ぶ)
const run = () => {
if (!swap.isActive()) swap.restart();
};
button.addEventListener("pointerenter", run);
button.addEventListener("focus", run);
});