デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
見出しの文字を 1 字ずつ、表と裏を重ねた小さな箱にします。箱を rotationX で 180° 回すと裏の色の文字が正面を向いて現れ、少し置いてもう半分回して表へ戻ります。カーソルを重ねている間だけ動く、ホバーの仕掛けです。
半回転は 0.55 秒の power2.inOut で、隣の文字と 0.08 秒ずつずらして送ります。透視の強さ(perspective)は 350 に固定していて、これより小さくすると近づく面が誇張されて安っぽく見えます。
裏の面には backface-visibility を指定します。無いと、回転の途中で裏返った文字が透けて重なります。1 字ずつに分けた文字は読み上げで 1 字ずつ聞こえてしまうため、元の文言はリンクの名前に移し、分けた文字のほうは読み上げから外します。
裏の色を変えるとめくれた印象が変わり、ずらす間隔を広げるほど波打って見えます。ナビゲーションのリンクや、短い見出しのホバー演出に向いています。
コード
<!-- 文言はそのまま書く。1 字ずつの箱に分けるのは script の役目 -->
<a class="flip-text" href="/works/">
<span class="flip-text__label">制作実績を見る</span>
</a>
.flip-text {
--ink: #262626; /* 表の文字 */
--back: #b27771; /* 裏の色。表より明るくすると、裏返ったのが明るさでもわかる */
display: inline-flex;
font-size: clamp(28px, 5vw, 56px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--ink);
text-decoration: none;
white-space: nowrap;
}
.flip-text__label {
display: flex;
}
.flip-text__space {
display: inline-block;
width: 0.32em;
}
/* 1 字ぶんの箱。preserve-3d で中の表と裏を立体として扱う */
.flip-text__char {
display: inline-block;
position: relative;
transform-style: preserve-3d;
}
.flip-text__face {
display: block;
backface-visibility: hidden;
}
/* 裏ははじめから伏せて重ねておく。箱が 180° 回ると正立して現れる */
.flip-text__face--back {
position: absolute;
inset: 0;
color: var(--back);
transform: rotateX(180deg);
}
.flip-text:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 6px;
}
import { gsap } from "gsap";
const HALF = 0.55; // 半回転(表 → 裏)にかける時間
const EACH = 0.08; // 隣の文字とのずれ
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".flip-text").forEach((link) => {
const label = link.querySelector(".flip-text__label");
const text = label.textContent;
// 1 字ずつに分けた span は 1 字ずつ読み上げられるので、元の文言はリンクの名前に移す
link.setAttribute("aria-label", text);
label.setAttribute("aria-hidden", "true");
label.textContent = "";
const flips = [];
for (const ch of text) {
if (ch === " ") {
const space = document.createElement("span");
space.className = "flip-text__space";
label.append(space);
continue;
}
const flip = document.createElement("span");
flip.className = "flip-text__char";
const front = document.createElement("span");
front.className = "flip-text__face";
front.textContent = ch;
// 裏は同じ箱の中心で伏せておく。箱が 180° 回ると、表と同じ位置に正立して現れる
const back = document.createElement("span");
back.className = "flip-text__face flip-text__face--back";
back.textContent = ch;
flip.append(front, back);
label.append(flip);
flips.push(flip);
}
gsap.set(flips, { transformPerspective: 350 });
const flip = gsap
.timeline({ paused: true })
.to(flips, { rotationX: 180, duration: HALF, ease: "power2.inOut", stagger: EACH });
// 動きを減らす設定では、ほぼ一瞬で裏返す
if (reduceMotion) flip.timeScale(20);
const open = () => flip.play();
const close = () => flip.reverse();
link.addEventListener("pointerenter", open);
link.addEventListener("pointerleave", close);
link.addEventListener("focus", open);
link.addEventListener("blur", close);
});