DEMO
右上のボタンを押すと、押した位置から新しい配色が円形に広がります。
CODE
<div class="demo-stage ctt-stage">
<div class="ctt-view" id="ctt-view">
<div class="ctt-scene" id="ctt-scene" data-theme="light">
<div class="ctt-bar">
<span class="ctt-logo">Motion Lab</span>
<button type="button" class="ctt-toggle" id="ctt-toggle" aria-pressed="false" aria-label="ダークモード">☀</button>
</div>
<h3 class="ctt-h">円が広がって切り替わるテーマ</h3>
<p class="ctt-p">右上のボタンを押すと、押した位置から新しい配色が円形に広がります。</p>
</div>
</div>
<p class="ctt-status" id="ctt-status" role="status" aria-live="polite"></p>
</div>
.ctt-stage { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.6rem; padding:1.5rem; background:#0B0B10; min-height:300px; }
.ctt-view { position:relative; width:100%; max-width:380px; border-radius:14px; overflow:hidden; }
.ctt-scene { padding:1rem 1.1rem 1.4rem; background:var(--ctt-bg); color:var(--ctt-fg); }
.ctt-scene[data-theme="light"] { --ctt-bg:#F7F5F2; --ctt-fg:#1A1A1A; --ctt-sub:#5A5A5A; --ctt-line:rgba(0,0,0,.12); }
.ctt-scene[data-theme="dark"] { --ctt-bg:#15151B; --ctt-fg:#EDEDED; --ctt-sub:#A1A1AA; --ctt-line:rgba(255,255,255,.14); }
.ctt-layer { position:absolute; inset:0; }
.ctt-bar { display:flex; align-items:center; justify-content:space-between; padding-bottom:.7rem; margin-bottom:.9rem; border-bottom:1px solid var(--ctt-line); }
.ctt-logo { font-family:'Oswald',sans-serif; font-size:.9rem; letter-spacing:.12em; }
.ctt-toggle { width:44px; height:44px; border-radius:50%; border:1px solid var(--ctt-line); background:transparent; color:var(--ctt-fg); font-size:1.1rem; cursor:pointer; }
.ctt-toggle:focus-visible { outline:2px solid #E91B89; outline-offset:2px; }
.ctt-h { margin:0 0 .4rem; font-size:1.05rem; }
.ctt-p { margin:0; font-size:.82rem; line-height:1.7; color:var(--ctt-sub); }
.ctt-status { margin:0; min-height:1.2em; color:#A1A1AA; font-size:.74rem; }
(function(){
var view = document.getElementById('ctt-view');
var scene = document.getElementById('ctt-scene');
var btn = document.getElementById('ctt-toggle');
var status = document.getElementById('ctt-status');
if (!view || !scene || !btn) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
var busy = false;
function apply(el, dark){
el.dataset.theme = dark ? 'dark' : 'light';
el.querySelector('.ctt-toggle').textContent = dark ? '☾' : '☀';
}
btn.addEventListener('click', function(){
if (busy) return;
var dark = scene.dataset.theme !== 'dark';
btn.setAttribute('aria-pressed', dark ? 'true' : 'false');
status.textContent = dark ? 'ダークモードにしました' : 'ライトモードにしました';
if (reduce.matches || !view.animate) { apply(scene, dark); return; }
busy = true;
var v = view.getBoundingClientRect(), b = btn.getBoundingClientRect();
var cx = b.left + b.width / 2 - v.left, cy = b.top + b.height / 2 - v.top;
var r = Math.hypot(Math.max(cx, v.width - cx), Math.max(cy, v.height - cy));
var layer = scene.cloneNode(true);
layer.removeAttribute('id');
layer.classList.add('ctt-layer');
layer.setAttribute('aria-hidden', 'true');
layer.inert = true;
layer.querySelector('.ctt-toggle').removeAttribute('id');
apply(layer, dark);
view.appendChild(layer);
var anim = layer.animate({ clipPath: ['circle(0px at ' + cx + 'px ' + cy + 'px)', 'circle(' + r + 'px at ' + cx + 'px ' + cy + 'px)'] },
{ duration: 550, easing: 'cubic-bezier(.22,1,.36,1)' });
anim.onfinish = function(){ apply(scene, dark); layer.remove(); busy = false; };
});
})();
AI PROMPT
ボタンを押した位置から新しい配色が円形に広がって、ライトモードとダークモードが切り替わる演出を作って。配色は data-theme="light" / "dark" ごとにカスタムプロパティ(背景・文字・補足の文字・線の色)で定義する。切り替えボタンは aria-label="ダークモード" と aria-pressed で状態を示し、高さと幅は44px。押したら、画面の部分(シーン)を cloneNode(true) で複製して新しい配色を付け、元の上に position:absolute; inset:0 で重ねる。複製は aria-hidden="true" と inert にして、読み上げやフォーカスの対象から外す。ボタンの中心を円の中心にし、そこから最も遠い角までの距離(Math.hypot)を半径として、複製に Web Animations API の element.animate で clip-path: circle(0px at X Y) → circle(半径 at X Y) を .55秒・cubic-bezier(.22,1,.36,1) で広げる。広がり終わったら元の配色を新しい配色に変え、複製を取り除く。アニメーション中の連打は無視する。切り替えたことは role="status" の段落で「ダークモードにしました」と伝える。prefers-reduced-motion: reduce の時と element.animate が使えない時は、円を広げずに即座に切り替える。ページ全体で行う場合は document.startViewTransition を使う方法も紹介する。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
ダークモードの切り替えボタンを押すと、押した場所を中心に新しい配色が円形に広がっていく演出です。Magic UI にも同じ系統のコンポーネント(Theme Toggler)があり、公式の説明では View Transitions API と clip-path を使ったテーマ切り替えとされています。
当サイトの View Transition API は画面の切り替えそのものを扱う演出、Reveal Mask は文字がマスクの下から立ち上がる演出、Toggle Switch はオン・オフのスイッチです。本ページは画面の一部の配色を、押した位置から広げて切り替える別テーマで、デモの枠の中だけで完結するよう Web Animations API で作っています。
配色が一瞬で変わると目が驚きますが、押した場所から広がると「自分の操作で変わった」ことが伝わります。ただし大きな面積が動く演出なので、動きを減らす設定の利用者には必ず即座の切り替えにしてください。
ページ全体を円形に切り替えるにはどうしますか?
ページ全体なら View Transitions API の document.startViewTransition() で、切り替え前後の画面を丸ごと扱えます。新しい画面側の疑似要素(::view-transition-new(root))に clip-path の circle() を広げるアニメーションをかけると、同じ演出になります。MDN の互換性データでは Chrome と Edge は111、Safari は18、Firefox は144から使えるとされています。対応していないブラウザでは即座に切り替えるよう分岐してください。
円の半径はどう決めていますか?
ボタンの中心から、表示範囲の4つの角のうち最も遠い角までの距離を半径にしています。横方向は左右の遠い方、縦方向は上下の遠い方を取り、Math.hypot で斜めの距離を求めます。これより小さいと角に古い配色が残り、大きすぎると広がりが速く見えてしまいます。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。