デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
アイコンだけの丸いボタンにカーソルを重ねると、そのボタンが右へ伸びてピル型になり、名前が読めるようになります。並びは flex なので、伸びたぶんだけ右隣のボタンは押されてずれます。文字は最初から丸の右側の外に置いてあり、ボタンの overflow: hidden で隠れているだけです。
幅は0.5秒の power3.out で広げ、文字はそれより0.12秒遅れて、少し左から滑り込みます。矢印のアイコンは幅と同時に45°回り、斜め右上から右向きに変わります。
幅の変化は width で動かすことにしました。scaleX のほうが手軽ですが、ボタン全体が横に引き伸ばされ、丸い端が楕円にゆがみます。角の丸みを直径の半分で固定しておけば、どこまで伸びても両端は半円のままです。伸びきったときの形を決めているのは文字の左の位置と右の余白で、それぞれ直径の0.86倍と0.34倍にしてあります。
閉じている間の文字は透明なだけなので、スクリーンリーダーではリンクの名前として読まれます。Tab でフォーカスが来たときも開き、ホバーのないタッチ端末でははじめから開いた形で並べます。
横に並べるときは、間隔を詰めすぎると、縮んでいくボタンの右隣がちょうどカーソルの下へ滑り込み、続けて開いてしまうことがあります。縦に並べれば、この連鎖は起きません。
コード
<!-- 閉じている間も、文字は透明にしてあるだけなので、リンクの名前として読み上げられる -->
<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);
});
});