グーイ・トグル
ノブが移動する瞬間、SVGフィルターで液体のようにトラックとつながって伸び縮みするトグルスイッチ。設定画面のON/OFF切替に使えます。
ライブデモ
コード
HTML
<!-- グーイ・トグル: ノブが移動する瞬間、SVGフィルターでトラックと液体状につながって伸びる -->
<div class="stage">
<svg width="0" height="0" style="position: absolute;" aria-hidden="true" focusable="false">
<defs>
<filter id="pf-goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="blur" />
<feColorMatrix in="blur" mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo" />
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</defs>
</svg>
<button class="gooey" type="button" role="switch" aria-checked="false" aria-label="通知を有効にする">
<span class="gooey__track" aria-hidden="true">
<span class="gooey__goo">
<span class="gooey__echo"></span>
<span class="gooey__knob"></span>
</span>
</span>
<span class="gooey__state" aria-hidden="true">OFF</span>
</button>
<p class="hint">クリックでON/OFFを切替</p>
</div>
CSS
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
overflow: hidden;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background: radial-gradient(circle at 50% 30%, #171b30 0%, #0d0e1a 72%);
color: #f4f5fb;
}
.stage { display: grid; place-items: center; gap: 18px; }
.gooey {
position: relative;
display: inline-flex;
align-items: center;
gap: 14px;
padding: 10px 16px;
border: none;
border-radius: 999px;
cursor: pointer;
background: rgba(255, 255, 255, .04);
box-shadow: 0 10px 26px rgba(0, 0, 0, .4);
transition: transform .15s ease;
}
.gooey:active { transform: scale(.97); }
.gooey:focus-visible { outline: 2px solid #67e8f9; outline-offset: 4px; }
/* アイドル時にほんのり光って触れられることを示唆する */
.gooey::before {
content: "";
position: absolute;
inset: -4px;
border-radius: inherit;
box-shadow: 0 0 0 rgba(103, 232, 249, 0);
animation: gooey-hint 3.6s ease-in-out infinite;
pointer-events: none;
}
.gooey[aria-checked="true"]::before { animation: none; }
@keyframes gooey-hint {
0%, 100% { box-shadow: 0 0 0 rgba(103, 232, 249, 0); }
50% { box-shadow: 0 0 14px rgba(103, 232, 249, .28); }
}
.gooey__track {
position: relative;
display: block;
width: 88px;
height: 44px;
border-radius: 999px;
background: #23263f;
border: 1px solid rgba(255, 255, 255, .08);
transition: background-color .45s ease, border-color .45s ease;
}
.gooey[aria-checked="true"] .gooey__track {
background: #0f3b34;
border-color: rgba(52, 211, 153, .35);
}
/* 液状フィルターはこのグループにだけ適用し、トラック自体はくっきり保つ */
.gooey__goo {
position: absolute;
inset: 0;
filter: url(#pf-goo);
}
.gooey__knob,
.gooey__echo {
position: absolute;
top: 4px;
left: 4px;
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(145deg, #c3c8dc, #9aa1bd);
}
.gooey__knob {
transition: left .5s cubic-bezier(.34, 1.56, .64, 1), background .4s ease;
}
.gooey[aria-checked="true"] .gooey__knob {
left: 48px;
background: linear-gradient(145deg, #34d399, #22d3ee);
}
.gooey__echo {
transform: scale(0);
transition: transform .45s ease-out;
}
.gooey__state {
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
color: rgba(244, 245, 251, .55);
min-width: 28px;
transition: color .3s ease;
}
.gooey[aria-checked="true"] .gooey__state { color: #34d399; }
.hint { margin: 0; font-size: 13px; color: rgba(244, 245, 251, .5); }
@media (prefers-reduced-motion: reduce) {
.gooey::before { animation: none; }
.gooey__knob { transition-duration: .15s; }
.gooey__echo { transition-duration: .01ms; }
}
JavaScript
// グーイ・トグル: 切替のたびにトラック側へ残る「滴」を縮小させ、SVGのgooフィルターで
// ノブと粘性的につながって離れていくように見せる
(() => {
const btn = document.querySelector('.gooey');
const knob = document.querySelector('.gooey__knob');
const echo = document.querySelector('.gooey__echo');
const state = document.querySelector('.gooey__state');
const hint = document.querySelector('.hint');
if (!btn || !knob || !echo) return; // null安全
const OFF_X = 4;
const ON_X = 48;
const OFF_BG = 'linear-gradient(145deg, #c3c8dc, #9aa1bd)';
const ON_BG = 'linear-gradient(145deg, #34d399, #22d3ee)';
let on = false;
const toggle = () => {
const wasOn = on;
on = !on;
// 直前の位置・色のまま滴(echo)を出現させ、reflow後に縮小トランジションを起動する
echo.style.transition = 'none';
echo.style.left = `${wasOn ? ON_X : OFF_X}px`;
echo.style.background = wasOn ? ON_BG : OFF_BG;
echo.style.transform = 'scale(1)';
void echo.offsetWidth; // reflow
echo.style.transition = '';
requestAnimationFrame(() => { echo.style.transform = 'scale(0)'; });
btn.setAttribute('aria-checked', String(on));
if (state) state.textContent = on ? 'ON' : 'OFF';
if (hint) hint.textContent = on ? 'もう一度クリックでOFFに戻せます' : 'クリックでON/OFFを切替';
};
btn.addEventListener('click', toggle);
})();
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「グーイ・トグル」の効果を追加してください。
# 追加してほしい効果
グーイ・トグル(遊べるUI)
ノブが移動する瞬間、SVGフィルターで液体のようにトラックとつながって伸び縮みするトグルスイッチ。設定画面のON/OFF切替に使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- グーイ・トグル: ノブが移動する瞬間、SVGフィルターでトラックと液体状につながって伸びる -->
<div class="stage">
<svg width="0" height="0" style="position: absolute;" aria-hidden="true" focusable="false">
<defs>
<filter id="pf-goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="7" result="blur" />
<feColorMatrix in="blur" mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 20 -9" result="goo" />
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</defs>
</svg>
<button class="gooey" type="button" role="switch" aria-checked="false" aria-label="通知を有効にする">
<span class="gooey__track" aria-hidden="true">
<span class="gooey__goo">
<span class="gooey__echo"></span>
<span class="gooey__knob"></span>
</span>
</span>
<span class="gooey__state" aria-hidden="true">OFF</span>
</button>
<p class="hint">クリックでON/OFFを切替</p>
</div>
【CSS】
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
overflow: hidden;
display: grid;
place-items: center;
font-family: "Segoe UI", system-ui, sans-serif;
background: radial-gradient(circle at 50% 30%, #171b30 0%, #0d0e1a 72%);
color: #f4f5fb;
}
.stage { display: grid; place-items: center; gap: 18px; }
.gooey {
position: relative;
display: inline-flex;
align-items: center;
gap: 14px;
padding: 10px 16px;
border: none;
border-radius: 999px;
cursor: pointer;
background: rgba(255, 255, 255, .04);
box-shadow: 0 10px 26px rgba(0, 0, 0, .4);
transition: transform .15s ease;
}
.gooey:active { transform: scale(.97); }
.gooey:focus-visible { outline: 2px solid #67e8f9; outline-offset: 4px; }
/* アイドル時にほんのり光って触れられることを示唆する */
.gooey::before {
content: "";
position: absolute;
inset: -4px;
border-radius: inherit;
box-shadow: 0 0 0 rgba(103, 232, 249, 0);
animation: gooey-hint 3.6s ease-in-out infinite;
pointer-events: none;
}
.gooey[aria-checked="true"]::before { animation: none; }
@keyframes gooey-hint {
0%, 100% { box-shadow: 0 0 0 rgba(103, 232, 249, 0); }
50% { box-shadow: 0 0 14px rgba(103, 232, 249, .28); }
}
.gooey__track {
position: relative;
display: block;
width: 88px;
height: 44px;
border-radius: 999px;
background: #23263f;
border: 1px solid rgba(255, 255, 255, .08);
transition: background-color .45s ease, border-color .45s ease;
}
.gooey[aria-checked="true"] .gooey__track {
background: #0f3b34;
border-color: rgba(52, 211, 153, .35);
}
/* 液状フィルターはこのグループにだけ適用し、トラック自体はくっきり保つ */
.gooey__goo {
position: absolute;
inset: 0;
filter: url(#pf-goo);
}
.gooey__knob,
.gooey__echo {
position: absolute;
top: 4px;
left: 4px;
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(145deg, #c3c8dc, #9aa1bd);
}
.gooey__knob {
transition: left .5s cubic-bezier(.34, 1.56, .64, 1), background .4s ease;
}
.gooey[aria-checked="true"] .gooey__knob {
left: 48px;
background: linear-gradient(145deg, #34d399, #22d3ee);
}
.gooey__echo {
transform: scale(0);
transition: transform .45s ease-out;
}
.gooey__state {
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
color: rgba(244, 245, 251, .55);
min-width: 28px;
transition: color .3s ease;
}
.gooey[aria-checked="true"] .gooey__state { color: #34d399; }
.hint { margin: 0; font-size: 13px; color: rgba(244, 245, 251, .5); }
@media (prefers-reduced-motion: reduce) {
.gooey::before { animation: none; }
.gooey__knob { transition-duration: .15s; }
.gooey__echo { transition-duration: .01ms; }
}
【JavaScript】
// グーイ・トグル: 切替のたびにトラック側へ残る「滴」を縮小させ、SVGのgooフィルターで
// ノブと粘性的につながって離れていくように見せる
(() => {
const btn = document.querySelector('.gooey');
const knob = document.querySelector('.gooey__knob');
const echo = document.querySelector('.gooey__echo');
const state = document.querySelector('.gooey__state');
const hint = document.querySelector('.hint');
if (!btn || !knob || !echo) return; // null安全
const OFF_X = 4;
const ON_X = 48;
const OFF_BG = 'linear-gradient(145deg, #c3c8dc, #9aa1bd)';
const ON_BG = 'linear-gradient(145deg, #34d399, #22d3ee)';
let on = false;
const toggle = () => {
const wasOn = on;
on = !on;
// 直前の位置・色のまま滴(echo)を出現させ、reflow後に縮小トランジションを起動する
echo.style.transition = 'none';
echo.style.left = `${wasOn ? ON_X : OFF_X}px`;
echo.style.background = wasOn ? ON_BG : OFF_BG;
echo.style.transform = 'scale(1)';
void echo.offsetWidth; // reflow
echo.style.transition = '';
requestAnimationFrame(() => { echo.style.transform = 'scale(0)'; });
btn.setAttribute('aria-checked', String(on));
if (state) state.textContent = on ? 'ON' : 'OFF';
if (hint) hint.textContent = on ? 'もう一度クリックでOFFに戻せます' : 'クリックでON/OFFを切替';
};
btn.addEventListener('click', toggle);
})();
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。