デモ
デモを読み込めませんでした。
仕様
- 用途
- カード
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
並んだカードを、カーソルの位置を中心にした 1 つの光で照らします。各カードは 2 重の要素で、外側の地(罫線の色)の上に内側の面を 1px 内側へ重ねると、外側の地が 1px の枠として残ります。枠の光は外側と面のあいだの層に、面の光は面の中の層に、どちらも radial-gradient で描きます。
光の中心は、カーソルの位置からカードごとの左上を引いた座標を、CSS 変数の --x と --y に入れて指定します。どのカードも同じ点を中心に塗るので、光源が 1 つに見え、枠の光が隣のカードにもまたがります。光の半径は枠が 384px、面が 536px で、枠の光を強く、面の光は 20% の濃さにとどめています。
ポインタの動きはカードごとではなく、グリッド全体の要素で拾います。カードごとに拾うと、カードの間のすき間で光の中心が止まり、隣のカードへ移ったときに飛びます。面の光を文字の下に置くには、面に isolation: isolate を付けて重なりの基準を作り、光の層を z-index −1 にします。
グリッドに入ったら光の層の透明度を 0.45 秒で 1 へ上げ、出たら 0.6 秒で消します(どちらも power2.out)。キーボードで焦点が入ったときは、そのカードの上寄りを照らします。ホバーの無いタッチ端末ではつながず、動きを減らす設定では光をほぼ一瞬で点けて消します。
枠を強く光らせるなら枠の光の中心の濃さを、光を広げるなら半径を上げます。光は落ち着いたスレートの色にし、枠の光の芯だけを文字の色にして、中心が白く光るようにしています。1 枚ずつ傾ける動きと違い、グリッド全体を 1 つの光で照らすので、機能紹介のグリッド・料金プラン・ダッシュボードのタイル・ベントーグリッドに向いています。
コード
<section class="platform" aria-labelledby="platform-title">
<div class="platform__head">
<div>
<p class="platform__label">PLATFORM</p>
<h2 class="platform__title" id="platform-title">公開に必要なものを、すべて。</h2>
</div>
<p class="platform__lead">ここに説明文が入ります。</p>
</div>
<!-- 光はグリッド全体で 1 つ。カードごとに、ポインタの位置を --x / --y に入れて塗る -->
<ul class="spots">
<li class="spot spot--wide">
<!-- 外側の地が 1px の枠になる。ring は枠の光、glow は面の光 -->
<span class="spot__ring" aria-hidden="true"></span>
<div class="spot__body">
<span class="spot__glow" aria-hidden="true"></span>
<div class="spot__row">
<span class="spot__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 12h4l3-7 4 14 3-7h4" /></svg></span>
<p class="spot__stat"><span class="spot__number">48,200</span>毎分のリクエスト数</p>
</div>
<div class="spot__row">
<div>
<!-- リンクの ::after をカード全体に広げ、カードのどこを押しても移る -->
<h3 class="spot__title"><a href="/platform/insights/">リアルタイム分析</a></h3>
<p class="spot__text">ここに説明文が入ります。</p>
</div>
<!-- 飾りの棒グラフ。高さは --h(最大に対する割合) -->
<div class="spot__chart" aria-hidden="true">
<span style="--h: 0.32"></span><span style="--h: 0.46"></span><span style="--h: 0.4"></span><span style="--h: 0.58"></span><span style="--h: 0.5"></span><span style="--h: 0.66"></span><span style="--h: 0.44"></span><span style="--h: 0.62"></span>
<span style="--h: 0.76"></span><span style="--h: 0.6"></span><span style="--h: 0.84"></span><span style="--h: 0.7"></span><span style="--h: 0.9"></span><span style="--h: 0.78"></span><span style="--h: 0.96"></span><span style="--h: 0.86"></span>
</div>
</div>
</div>
</li>
<li class="spot">
<span class="spot__ring" aria-hidden="true"></span>
<div class="spot__body">
<span class="spot__glow" aria-hidden="true"></span>
<span class="spot__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" /></svg></span>
<div>
<h3 class="spot__title"><a href="/platform/edge/">世界中へ高速配信</a></h3>
<p class="spot__text">ここに説明文が入ります。</p>
</div>
</div>
</li>
<li class="spot">
<span class="spot__ring" aria-hidden="true"></span>
<div class="spot__body">
<span class="spot__glow" aria-hidden="true"></span>
<span class="spot__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 12a8 8 0 1 0 2.6-5.9L4 8.5" /><path d="M4 4v4.5h4.5" /></svg></span>
<div>
<h3 class="spot__title"><a href="/platform/rollbacks/">ワンクリックで元に戻す</a></h3>
<p class="spot__text">ここに説明文が入ります。</p>
</div>
</div>
</li>
<li class="spot">
<span class="spot__ring" aria-hidden="true"></span>
<div class="spot__body">
<span class="spot__glow" aria-hidden="true"></span>
<span class="spot__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1" /><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1" /></svg></span>
<div>
<h3 class="spot__title"><a href="/platform/previews/">プレビュー用のURL</a></h3>
<p class="spot__text">ここに説明文が入ります。</p>
</div>
</div>
</li>
<li class="spot">
<span class="spot__ring" aria-hidden="true"></span>
<div class="spot__body">
<span class="spot__glow" aria-hidden="true"></span>
<span class="spot__icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 3l7 3v6c0 4.5-3 7.8-7 9-4-1.2-7-4.5-7-9V6z" /><path d="M9 12l2 2 4-4" /></svg></span>
<div>
<h3 class="spot__title"><a href="/platform/security/">標準で安全</a></h3>
<p class="spot__text">ここに説明文が入ります。</p>
</div>
</div>
</li>
</ul>
</section>
.platform {
--ground: #111111; /* 地 */
--surface: #262626; /* カードの面 */
--ink: #eeece6; /* 文字。枠の光の芯もこの色 */
--light: #93b2c9; /* 光と棒グラフの色 */
--mute: #a8a6a0; /* 説明とラベル */
--line: rgba(238, 236, 230, 0.14); /* 休みの枠 */
--ring-size: 384px; /* 枠の光の半径 */
--glow-size: 536px; /* 面の光の半径 */
padding: 64px clamp(16px, 6vw, 96px);
background: var(--ground);
color: var(--ink);
}
.platform__head {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-end;
gap: 16px 32px;
margin-bottom: 48px;
}
.platform__label { margin: 0; color: var(--mute); font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.14em; }
.platform__title { margin: 16px 0 0; font-size: clamp(32px, 5vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; word-break: auto-phrase; }
.platform__lead { margin: 0; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; }
.spots { display: grid; grid-template-columns: 1fr; gap: 16px; margin: 0; padding: 0; list-style: none; }
@media (min-width: 720px) {
.spots { grid-template-columns: repeat(2, 1fr); }
.spot--wide { grid-column: span 2; }
}
@media (min-width: 1080px) {
.spots { grid-template-columns: repeat(3, 1fr); }
}
/* 外側の地が 1px の枠になる。枠の光(ring)はその上、面(body)の下に重ねる。grid で面を行の高さまで伸ばす */
.spot { --x: 50%; --y: 50%; position: relative; display: grid; border-radius: 4px; background: var(--line); }
.spot__ring,
.spot__glow { position: absolute; inset: 0; border-radius: inherit; opacity: 0; pointer-events: none; }
.spot__ring {
background: radial-gradient(var(--ring-size) circle at var(--x) var(--y), var(--ink), color-mix(in srgb, var(--light) 60%, transparent) 18%, color-mix(in srgb, var(--light) 20%, transparent) 42%, transparent 70%);
}
/* isolation で重なりの基準を作り、面の光(z-index: -1)を面の色の上・文字の下に置く */
.spot__body {
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
box-sizing: border-box;
min-height: 222px;
margin: 1px;
padding: clamp(24px, 5vw, 32px);
border-radius: 3px;
background: var(--surface);
}
.spot__glow {
z-index: -1;
background: radial-gradient(var(--glow-size) circle at var(--x) var(--y), color-mix(in srgb, var(--light) 20%, transparent), color-mix(in srgb, var(--light) 7%, transparent) 36%, transparent 70%);
}
.spot__row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-end; gap: 24px; }
.spot__row:first-of-type { align-items: flex-start; }
.spot__icon { display: grid; place-items: center; align-self: flex-start; width: 40px; height: 40px; border-radius: 4px; box-shadow: inset 0 0 0 1px var(--line); }
.spot__icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.spot__title { margin: 0; font-size: 20px; font-weight: 700; line-height: 1.3; letter-spacing: -0.01em; }
.spot__title a { color: inherit; text-decoration: none; }
/* カード全体を押せるようにする */
.spot__title a::after { content: ""; position: absolute; inset: 0; border-radius: 3px; }
.spot__title a:focus-visible { outline: none; }
.spot__title a:focus-visible::after { outline: 2px solid var(--ink); outline-offset: 4px; }
/* 狭い画面で折り返すときも、文節の切れ目で分ける */
.spot__text { margin: 8px 0 0; color: var(--mute); font-size: 14px; line-height: 1.7; letter-spacing: 0.04em; word-break: auto-phrase; }
.spot__stat { margin: 0; color: var(--mute); font-size: 13px; line-height: 1.4; letter-spacing: 0.04em; text-align: right; }
.spot__number { display: block; margin-bottom: 8px; color: var(--ink); font-size: 32px; font-weight: 700; line-height: 1; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
/* 狭い画面では棒を細くして幅に収める */
.spot__chart { display: flex; flex: 0 1 312px; min-width: 0; align-items: flex-end; gap: 8px; height: 96px; }
.spot__chart span { flex: 1; max-width: 12px; height: calc(var(--h) * 100%); border-radius: 4px 4px 1px 1px; background: linear-gradient(180deg, color-mix(in srgb, var(--light) 90%, transparent), color-mix(in srgb, var(--light) 12%, transparent)); }
import { gsap } from "gsap";
// 指で触る端末にはカーソルが無いので、ポインタではつながない(キーボードの焦点には応える)
const canHover = window.matchMedia("(hover: hover)").matches;
// 動きを減らす設定では、光をほぼ一瞬で点けて消す
const speed = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 20 : 1;
document.querySelectorAll(".spots").forEach((grid) => {
const cards = [...grid.querySelectorAll(".spot")];
const lights = grid.querySelectorAll(".spot__ring, .spot__glow");
// どのカードも同じ点を中心に塗るので、光源が 1 つに見え、枠の光が隣のカードにもまたがる
const aim = (x, y) => {
for (const card of cards) {
const rect = card.getBoundingClientRect(); // カードが多いなら、入ったときと resize のときに測っておく
card.style.setProperty("--x", `${x - rect.left}px`);
card.style.setProperty("--y", `${y - rect.top}px`);
}
};
const fade = (on) => {
gsap.to(lights, { opacity: on ? 1 : 0, duration: (on ? 0.45 : 0.6) / speed, ease: "power2.out", overwrite: "auto" });
};
if (canHover) {
// グリッドの要素で拾うので、カードの間のすき間の上でも光が付いてくる
grid.addEventListener("pointermove", (event) => aim(event.clientX, event.clientY));
grid.addEventListener("pointerenter", () => fade(true));
grid.addEventListener("pointerleave", () => {
if (!grid.querySelector(":focus-visible")) fade(false);
});
}
// キーボードで送ったときは、焦点のあるカードの上寄りを照らす(マウスで押したときの焦点では動かさない)
grid.addEventListener("focusin", (event) => {
if (!event.target.matches(":focus-visible")) return;
const rect = event.target.closest(".spot").getBoundingClientRect();
aim(rect.left + rect.width / 2, rect.top + rect.height * 0.3);
fade(true);
});
grid.addEventListener("focusout", (event) => {
if (!grid.contains(event.relatedTarget) && !(canHover && grid.matches(":hover"))) fade(false);
});
});