本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
オーバーレイ

ホバーでツールチップの吹き出しが矢印の先から弾んで出る

デモ

操作:カーソルを重ねる。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

吹き出しを、下端の矢印の先を起点に小さく縮めて隠しておき、ポインタが重なったら弾ませながら膨らませます。離れたら、同じタイムラインを速めに逆再生して消します。ボタンの地と色も、重なっているあいだだけ明るくします。

重ねてから 0.2 秒待って出します。待たずに出すと、ツールバーの上を横切っただけで吹き出しが次々に出て、うるさく見えます。大きさは 0.5 倍から、位置は 10px 下から、0.45 秒の back.out(2.6) で少し行き過ぎてから収まります。消えるときは 2.5 倍の速さでたどるので、隣のボタンへ移ったときに前の吹き出しが残りません。

拡大の起点は transform-origin で矢印の先に置きます。既定の中央のままだと、吹き出しが宙で膨らみ、矢印の先がボタンから離れたり近づいたりします。横の中央そろえを CSS の transform で書くと、GSAP がそれを px の移動量として読み込み、幅が変わったときにずれるので、xPercent に -50 を渡します。

吹き出しは role="tooltip" にして、ボタンの aria-labelledby で結びます。アイコンだけのボタンでは、吹き出しの文がそのままボタンの名前になるからです。説明として結んだうえでボタンにも名前を付けると、同じ語が 2 度読み上げられます。キーボードで焦点が当たったときは待たずに出し、焦点が外れたら消します。Esc でも消せるようにし、ボタンと吹き出しを 1 つの要素で包んで、吹き出しの上へポインタを動かしても消えないようにします。

弾みを抑えたいときは、行き過ぎの強さを 1.5 ほどに下げ、始まりの大きさを 0.8 倍ほどに寄せます。待つ時間は 0.1〜0.3 秒のあいだで決めます。短すぎると横切っただけで出て、長すぎると反応が鈍く感じられます。書式のツールバー・アイコンだけのボタン・グラフの点の説明に向いています。

コード

HTML
<div class="format-bar" role="group" aria-label="文字の書式">
  <!-- ボタンと吹き出しを 1 つに包み、包みに重なっている間は出したままにする(吹き出しの上へ動かしても消えない) -->
  <span class="tip">
    <button class="tip__button" type="button" aria-labelledby="tip-bold">
      <span class="tip__glyph" aria-hidden="true">B</span>
    </button>
    <span class="tip__bubble" id="tip-bold" role="tooltip">太字<kbd>⌘B</kbd></span>
  </span>
  <span class="tip">
    <button class="tip__button" type="button" aria-labelledby="tip-italic">
      <span class="tip__glyph tip__glyph--italic" aria-hidden="true">I</span>
    </button>
    <span class="tip__bubble" id="tip-italic" role="tooltip">斜体<kbd>⌘I</kbd></span>
  </span>
  <span class="tip">
    <button class="tip__button" type="button" aria-labelledby="tip-link">
      <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false">
        <g transform="rotate(-45 12 12)" fill="none" stroke="currentColor" stroke-width="2.4">
          <rect x="2.5" y="8.5" width="11" height="7" rx="3.5"/>
          <rect x="10.5" y="8.5" width="11" height="7" rx="3.5"/>
        </g>
      </svg>
    </button>
    <span class="tip__bubble" id="tip-link" role="tooltip">リンクを挿入<kbd>⌘K</kbd></span>
  </span>
</div>

CSS
.format-bar {
  --surface: #262626; /* ツールバーの面 */
  --ink: #eeece6; /* 重ねたときのアイコンと、吹き出しの地 */
  --idle: #a8a6a0; /* 休んでいるアイコン */
  --hover: rgba(238, 236, 230, 0.1); /* 重ねたときのボタンの地 */
  --line: rgba(238, 236, 230, 0.14); /* ツールバーの縁 */
  --tip-ink: #111111; /* 吹き出しの文字 */
  --arrow: 6px; /* 吹き出しの矢印の高さ */
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  border: 1px solid var(--line);
  border-radius: 4px;
  background-color: var(--surface);
}

.tip {
  position: relative;
  display: inline-flex;
}

.tip__button {
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background-color: transparent;
  color: var(--idle);
  font: inherit;
  font-size: 18px; /* font: inherit の後に書く(先だと打ち消される) */
  font-weight: 800;
  cursor: pointer;
  transition: background-color 0.15s linear, color 0.15s linear;
}

.tip:hover .tip__button,
.tip__button:focus-visible {
  background-color: var(--hover);
  color: var(--ink);
}

.tip__button:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.tip__glyph--italic { font-style: italic; font-weight: 700; }

/* 吹き出し。ボタンの真上に置き、矢印の先を拡大の起点にする(横の中央そろえは JS の xPercent) */
.tip__bubble {
  position: absolute;
  left: 50%;
  bottom: calc(100% + var(--arrow) + 6px);
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 4px;
  background-color: var(--ink);
  color: var(--tip-ink);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
  transform-origin: 50% calc(100% + var(--arrow));
  visibility: hidden; /* 出し入れは GSAP の autoAlpha */
}

/* 矢印(吹き出しと同じ地の色の三角)と、ボタンとのすき間を埋める透明な橋 */
.tip__bubble::after,
.tip__bubble::before {
  content: "";
  position: absolute;
  top: 100%;
}

.tip__bubble::after {
  left: calc(50% - var(--arrow));
  border: var(--arrow) solid transparent;
  border-bottom: 0;
  border-top-color: var(--ink);
}

.tip__bubble::before { left: 0; right: 0; height: calc(var(--arrow) + 6px); }

.tip__bubble kbd {
  padding: 2px 6px;
  border-radius: 4px;
  background-color: rgba(17, 17, 17, 0.08);
  color: #6b6b6b;
  font: inherit;
  letter-spacing: 0.02em;
}

@media (prefers-reduced-motion: reduce) {
  .tip__button { transition: none; }
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = reduceMotion ? 20 : 1; // 動きを減らす設定では、ほぼ一瞬で切り替える
const DELAY = 0.2; // 重ねてから出るまで。通り過ぎただけのボタンでは出さない
const HIDE_SPEED = 2.5; // 消えるときは、出るときの 2.5 倍の速さで逆にたどる

document.querySelectorAll(".tip").forEach((tip) => {
  const button = tip.querySelector(".tip__button");
  const bubble = tip.querySelector(".tip__bubble");

  // 横の中央そろえは xPercent で持たせる。CSS の transform に書くと、GSAP が px に読み替えてずれる
  gsap.set(bubble, { xPercent: -50 });
  const pop = gsap
    .timeline({ paused: true })
    .fromTo(bubble, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.15, ease: "power1.out" }, 0)
    // 矢印の先(transform-origin)を起点に、少し下から弾みながら膨らむ
    .fromTo(bubble, { scale: 0.5, y: 10 }, { scale: 1, y: 0, duration: 0.45, ease: "back.out(2.6)" }, 0);

  let wait;
  const show = (delay) => {
    wait?.kill();
    wait = gsap.delayedCall(delay, () => pop.timeScale(SPEED).play());
  };
  const hide = () => {
    wait?.kill();
    pop.timeScale(SPEED * HIDE_SPEED).reverse();
  };
  // キーボードで焦点を当てているか(押して付いた焦点は数えない)
  const keyboardFocus = () => button.matches(":focus-visible");

  tip.addEventListener("pointerenter", () => show(DELAY));
  tip.addEventListener("pointerleave", () => keyboardFocus() || hide());
  button.addEventListener("focus", () => keyboardFocus() && show(0)); // キーボードでは待たずに出す
  button.addEventListener("blur", hide);
  // Esc で消せるようにする(重ねたままでも、焦点を当てたままでも)
  document.addEventListener("keydown", (event) => event.key === "Escape" && hide());
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。