デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
文字を 1 字ずつ、高さが 1 行ぶんの窓に入れます。窓の中には同じ字を縦に 2 つ積んであり、下の字は窓の外に隠れています。カーソルが重なったら、この列を 1 字ぶんだけ持ち上げます。上の字が窓から抜け、入れ替わりに下から同じ字が入ってきます。
持ち上げは 0.45 秒の power3.inOut で、左の字から 0.02 秒ずつ遅らせます。7 字なら端から端まで 0.12 秒の差がつき、波のように入れ替わります。離れたときは同じタイムラインを逆再生するので、今度は右の字から戻ります。
窓の高さは、行の高さと同じ値にします。line-height だけを変えて窓と食い違わせると、下に積んだ字の上端が窓からのぞきます。語と語のあいだの空白も、そのままだと窓の幅が潰れるので、空白をそのまま残す指定(white-space を pre)にします。
分割は自前で行うので、文字を分けるプラグインも、書体の読み込みを待つ処理も要りません。ただし 1 字ずつに分けた span は 1 字ずつ読み上げられてしまうので、元の文言はリンクの名前(aria-label)に移し、分けた文字のほうは読み上げから外します。
下の段だけ色を変えると、文字が入れ替わると同時に色も変わります。和文も 1 字ずつ窓に分ければそのまま使えます(語の区切りを探す必要がありません)。ナビゲーションのリンクや、申し込みのボタンのように、短い文言のところに向いています。
コード
<!-- 文言はそのまま書く。1 字ずつの窓に分けるのは script の役目 -->
<a class="text-roll" href="/contact/">
<span class="text-roll__label">ご相談はこちら</span>
</a>
.text-roll {
--ink: #262626; /* 地の色 */
--on-ink: #ffffff; /* 文字の色 */
--line: 20px; /* 1 字の窓の高さ。行の高さと同じ値にする */
display: inline-flex;
align-items: center;
height: 48px;
padding: 0 32px;
border-radius: 999px;
background-color: var(--ink);
color: var(--on-ink);
font-size: 15px;
font-weight: 600;
line-height: var(--line);
letter-spacing: 0.04em;
text-decoration: none;
white-space: nowrap;
}
.text-roll__label {
display: flex;
}
/* 1 字ぶんの窓。行の高さと違う値にすると、下に積んだ字が窓からのぞく */
.text-roll__cell {
display: block;
height: var(--line);
overflow: hidden;
}
.text-roll__stack {
display: block;
}
/* 同じ字を縦に 2 つ積む。pre にしないと、語と語のあいだの空白が潰れて幅が無くなる */
.text-roll__stack > span {
display: block;
white-space: pre;
}
.text-roll: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(".text-roll").forEach((button) => {
const label = button.querySelector(".text-roll__label");
const text = label.textContent.trim();
// 1 字ずつの span は 1 字ずつ読み上げられてしまうので、元の文言はリンクの名前に移す
button.setAttribute("aria-label", text);
label.setAttribute("aria-hidden", "true");
// 1 字ぶんの窓を作り、同じ字を縦に 2 つ積む。下の字は窓の外に隠れている
label.textContent = "";
const stacks = [...text].map((char) => {
const cell = document.createElement("span");
cell.className = "text-roll__cell";
const stack = document.createElement("span");
stack.className = "text-roll__stack";
for (let i = 0; i < 2; i++) {
const glyph = document.createElement("span");
glyph.textContent = char;
stack.append(glyph);
}
cell.append(stack);
label.append(cell);
return stack;
});
// 積んだ列を 1 字ぶん(列の高さの半分)持ち上げる。左の字から 0.02 秒ずつ遅らせる
const hover = gsap
.timeline({ paused: true })
.to(stacks, { yPercent: -50, duration: 0.45, ease: "power3.inOut", stagger: 0.02 });
// 動きを減らす設定では、ほぼ一瞬で入れ替える
if (reduceMotion) hover.timeScale(20);
const roll = () => hover.play();
const back = () => hover.reverse();
button.addEventListener("pointerenter", roll);
button.addEventListener("pointerleave", back);
button.addEventListener("focus", roll);
button.addEventListener("blur", back);
});