本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ボタン

ホバーで丸いボタンが伸び、文字が現れる

デモ

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

仕様

用途
ボタン
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

丸いアイコンのボタンを横に並べ、カーソルが重なったら幅を広げてピルの形にします。文字は丸の右の外に置き、ボタンの overflow: hidden で切り抜いてあるので、幅が広がるにつれて見えてきます。並びは flex なので、伸びたボタンが右隣を押し出します。

幅は 0.5 秒の power3.out で、丸の直径から「文字の左の位置 + 文字の幅 + 右の余白」まで広げます。文字は 0.12 秒遅れて、少し左から滑り込みながら現れ、矢印のアイコンは同時に 45° 回って、斜め上から右向きに変わります。文字の左の位置は直径の 0.86 倍、右の余白は 0.34 倍で、この 2 つで伸びたときの形が決まります。

伸ばすのは width です。scaleX で伸ばすと、丸い端が楕円に歪みます。角の丸みを直径の半分の固定値にしておけば、どこまで伸びても端は半円のままです。横に並べると、縮んでいくボタンの右隣がカーソルの下へ滑り込んできて、続けて開くことがあるので、隙間を狭くしすぎないようにします(縦に並べれば起きません)。

閉じている間も文字は透明にしてあるだけなので、リンクの名前として読み上げられます。キーボードで移ってきたとき(focus)にも開き、ホバーの無い指の端末では、はじめから開いた形で並べます。問い合わせ・メール・予約のようなアイコンボタンの並びや、フッターの CTA、画面の隅に固定するボタンに向いています。

コード

HTML
<!-- 閉じている間も、文字は透明にしてあるだけなので、リンクの名前として読み上げられる -->
<ul class="pill-row">
  <li>
    <a class="pill pill--primary" href="/contact/">
      <span class="pill__icon" aria-hidden="true">
        <svg class="pill__arrow" viewBox="0 0 24 24"><path d="M7 17L17 7M9 7h8v8" /></svg>
      </span>
      <span class="pill__label">制作を依頼する</span>
    </a>
  </li>
  <li>
    <a class="pill" href="mailto:sample@higashiyuki.com">
      <span class="pill__icon" aria-hidden="true">
        <svg viewBox="0 0 24 24"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" /><path d="M4 7l8 6 8-6" /></svg>
      </span>
      <span class="pill__label">sample@higashiyuki.com</span>
    </a>
  </li>
  <li>
    <a class="pill" href="/booking/">
      <span class="pill__icon" aria-hidden="true">
        <svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="15" rx="2.5" /><path d="M4 10h16M8.5 3v4M15.5 3v4" /></svg>
      </span>
      <span class="pill__label">打ち合わせを予約</span>
    </a>
  </li>
</ul>

CSS
/* flex で横に並べる。1 つが伸びると右隣を押し出す。狭い画面では次の行へ送る */
.pill-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pill {
  --size: 64px;      /* 丸の直径 */
  --ink: #262626;    /* 線と文字の色 */
  --on-ink: #ffffff; /* 塗った丸の上の文字の色 */
  position: relative;
  display: block;
  width: var(--size);
  height: var(--size);
  /* 丸みは直径の半分の固定値。幅が伸びても端は半円のまま */
  border-radius: calc(var(--size) / 2);
  box-shadow: inset 0 0 0 1px var(--ink);
  color: var(--ink);
  overflow: hidden;
  text-decoration: none;
}

.pill--primary {
  background-color: var(--ink);
  box-shadow: none;
  color: var(--on-ink);
}

.pill__icon {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  place-items: center;
  width: var(--size);
  height: var(--size);
}

.pill__icon svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* 丸の右の外に置き、ボタンの overflow: hidden で切り抜く。右の余白も文字の箱に含める */
.pill__label {
  position: absolute;
  top: 0;
  left: calc(var(--size) * 0.86);
  padding-right: calc(var(--size) * 0.34);
  font-size: 15px;
  font-weight: 600;
  line-height: var(--size);
  letter-spacing: 0.04em;
  white-space: nowrap;
  opacity: 0;
}

.pill:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const canHover = window.matchMedia("(hover: hover)").matches;

// 伸びたときの幅は文字の幅で決まるので、書体が確定してから測る
document.fonts.ready.then(() => {
  document.querySelectorAll(".pill").forEach((pill) => {
    const label = pill.querySelector(".pill__label");
    const arrow = pill.querySelector(".pill__arrow");
    const size = pill.offsetHeight;
    // 文字の左の位置 + 文字の幅 + 右の余白(余白は文字の箱の padding-right)
    const open = Math.round(label.offsetLeft + label.offsetWidth);

    const expand = gsap
      .timeline({ paused: true })
      // scaleX で伸ばすと丸い端が楕円に歪むので、幅そのものを動かす
      .fromTo(pill, { width: size }, { width: open, duration: 0.5, ease: "power3.out" }, 0)
      // 文字は少し遅れて、左から滑り込ませる
      .fromTo(label, { opacity: 0, x: -size * 0.12 }, { opacity: 1, x: 0, duration: 0.35, ease: "power2.out" }, 0.12);
    // 矢印は斜め上(↗)から右向き(→)へ回す
    if (arrow) expand.fromTo(arrow, { rotation: 0 }, { rotation: 45, duration: 0.45, ease: "power3.out" }, 0);

    // ホバーの無い指の端末では、はじめから開いた形で並べる
    if (!canHover) {
      expand.progress(1);
      return;
    }
    // 動きを減らす設定では、ほぼ一瞬で開け閉めする
    if (reduceMotion) expand.timeScale(20);

    const show = () => expand.play();
    const hide = () => expand.reverse();
    pill.addEventListener("pointerenter", show);
    pill.addEventListener("pointerleave", hide);
    // キーボードで移ってきたときも開く
    pill.addEventListener("focus", show);
    pill.addEventListener("blur", hide);
  });
});

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

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