デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- 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、画面の隅に固定するボタンに向いています。
コード
<!-- 閉じている間も、文字は透明にしてあるだけなので、リンクの名前として読み上げられる -->
<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>
/* 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;
}
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);
});
});