DEMO
CODE
<div class="demo-stage ivk-stage">
<div class="ivk-bar">
<button type="button" class="ivk-btn ivk-primary" commandfor="ivk-dialog" command="show-modal">ダイアログを開く</button>
<button type="button" class="ivk-btn" commandfor="ivk-card" command="--flip">カードを裏返す</button>
</div>
<div class="ivk-card" id="ivk-card">
<div class="ivk-inner">
<div class="ivk-face">FRONT</div>
<div class="ivk-face ivk-back">BACK</div>
</div>
</div>
<p class="ivk-status" id="ivk-status" role="status" aria-live="polite"></p>
<dialog class="ivk-dialog" id="ivk-dialog" aria-labelledby="ivk-title">
<h3 class="ivk-title" id="ivk-title">変更を保存しますか?</h3>
<p class="ivk-desc">押したボタンの value が returnValue として受け取れます。</p>
<div class="ivk-actions">
<button type="button" class="ivk-btn" commandfor="ivk-dialog" command="close" value="cancel">キャンセル</button>
<button type="button" class="ivk-btn ivk-primary" commandfor="ivk-dialog" command="close" value="save">保存する</button>
</div>
</dialog>
</div>
.ivk-stage { position:relative; overflow:hidden; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1rem; padding:2rem 1.25rem; background:#0B0B10; min-height:320px; }
.ivk-bar { display:flex; gap:.6rem; flex-wrap:wrap; justify-content:center; }
.ivk-btn { min-height:44px; padding:0 1.1rem; border-radius:10px; border:1px solid rgba(255,255,255,.22); background:transparent; color:#EDEDED; font:inherit; font-size:.86rem; cursor:pointer; }
.ivk-primary { border-color:transparent; background:#D1177A; color:#fff; font-weight:600; }
.ivk-btn:focus-visible { outline:2px solid #fff; outline-offset:3px; }
.ivk-card { position:relative; width:160px; height:96px; perspective:600px; }
/* 回すのは内側の箱だけ。面は preserve-3d の中で裏向きの時に隠れる(Safari でも崩れない定番の組み方) */
.ivk-inner { position:relative; width:100%; height:100%; transform-style:preserve-3d; transition: transform .6s cubic-bezier(.22,1,.36,1); }
.ivk-face { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; border-radius:12px; background:linear-gradient(135deg,#7B37A8,#D1177A); color:#fff; font-weight:700; letter-spacing:.2em; -webkit-backface-visibility:hidden; backface-visibility:hidden; }
.ivk-back { background:linear-gradient(135deg,#0369A1,#1E3A8A); transform:rotateY(180deg); }
.ivk-card.is-flipped .ivk-inner { transform:rotateY(180deg); }
.ivk-status { margin:0; min-height:1.3em; color:#A1A1AA; font-size:.8rem; text-align:center; }
/* ダイアログ:開く時は @starting-style から、閉じる時は display と overlay を最後に切り替えて消える動きを見せる */
.ivk-dialog { margin:auto; width:min(320px, calc(100% - 2rem)); box-sizing:border-box; padding:1.2rem; border:1px solid rgba(255,255,255,.12); border-radius:14px; background:#1E1E26; color:#EDEDED;
opacity:0; transform:translateY(12px) scale(.96);
transition: opacity .25s, transform .25s cubic-bezier(.22,1,.36,1), overlay .25s allow-discrete, display .25s allow-discrete; }
.ivk-dialog[open] { opacity:1; transform:none; }
@starting-style { .ivk-dialog[open] { opacity:0; transform:translateY(12px) scale(.96); } }
.ivk-dialog::backdrop { background:rgb(0 0 0 / 0); transition: background-color .25s, overlay .25s allow-discrete, display .25s allow-discrete; }
.ivk-dialog[open]::backdrop { background:rgb(0 0 0 / .6); }
@starting-style { .ivk-dialog[open]::backdrop { background:rgb(0 0 0 / 0); } }
.ivk-title { margin:0 0 .4rem; color:#fff; font-size:1rem; }
.ivk-desc { margin:0; color:#C9C9D1; font-size:.8rem; line-height:1.7; }
.ivk-actions { display:flex; justify-content:flex-end; gap:.5rem; margin-top:1rem; flex-wrap:wrap; }
@media (prefers-reduced-motion: reduce) {
.ivk-inner, .ivk-dialog, .ivk-dialog::backdrop { transition:none; }
}
(function(){
var card = document.getElementById('ivk-card');
var dlg = document.getElementById('ivk-dialog');
var status = document.getElementById('ivk-status');
if (!card || !dlg || !status) return;
function flip(){
var on = card.classList.toggle('is-flipped');
status.textContent = 'カードを' + (on ? '裏' : '表') + 'にしました(--flip 命令)';
}
/* 独自の命令(-- で始まる値)は、commandfor で指した要素に command イベントとして届く */
card.addEventListener('command', function(e){ if (e.command === '--flip') flip(); });
var viaEsc = false;
dlg.addEventListener('cancel', function(){ viaEsc = true; }); /* Esc などの閉じる要求 */
dlg.addEventListener('close', function(){
status.textContent = 'ダイアログを閉じました(returnValue: ' + (viaEsc ? 'esc' : (dlg.returnValue || 'なし')) + ')';
viaEsc = false; dlg.returnValue = '';
});
/* command 未対応のブラウザだけ、同じ属性を読んでクリックで代わりに動かす */
if ('command' in HTMLButtonElement.prototype) return;
document.querySelectorAll('.ivk-stage button[commandfor]').forEach(function(b){
b.addEventListener('click', function(){
var t = document.getElementById(b.getAttribute('commandfor'));
var c = b.getAttribute('command');
if (!t) return;
if (c === 'show-modal' && !t.open) t.showModal();
else if (c === 'close' && t.open) t.close(b.value);
else if (c === '--flip') flip();
});
});
})();
AI PROMPT
button の commandfor と command 属性で、JavaScript を書かずにダイアログを開閉するデモを作って。「ダイアログを開く」ボタンに commandfor="ダイアログのid" command="show-modal" を付け、dialog の中の「キャンセル」「保存する」ボタンには command="close" と value="cancel" / value="save" を付ける(押したボタンの value が dialog.returnValue になる)。dialog の close イベントで returnValue を role="status" の欄に表示し、Esc で閉じた時は cancel イベントでフラグを立てて「esc」と表示する(Esc では returnValue が変わらないため)。表示したら returnValue を空に戻し、前回の値が残らないようにする。ページ側の CSS で * { margin:0 } のようなリセットをしていると、モーダルの dialog が中央に来ない(UA の margin:auto が消える)ので、dialog に margin:auto を付け直す。もう1つのボタンに command="--flip"(-- で始まる独自の命令)と commandfor="カードのid" を付け、カードの command イベントで e.command が '--flip' の時に表裏を切り替える(カードの中に transform-style: preserve-3d の内側の箱を置き、その箱だけを rotateY(180deg) へ 0.6秒で回す。表と裏の面は backface-visibility:hidden(Safari 向けに -webkit- 付きも)にし、裏の面は最初から rotateY(180deg) にしておく。面を1枚ずつ回す組み方は Safari で裏面が鏡文字で残ることがある)。ダイアログは opacity と transform に transition を付け、[open] で表示、@starting-style で開く瞬間の始まりの値を指定し、overlay と display に allow-discrete を付けて閉じる時も消える動きを見せる。::backdrop も同じ方法で背景を暗くする。'command' in HTMLButtonElement.prototype が false のブラウザだけ、commandfor と command を読んで click で showModal() / close(value) / 裏返しを呼ぶ代わりの処理を入れる。prefers-reduced-motion: reduce の時は transition を無効化する。
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アソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
CREATOR TOOLS · スクール(PR)
※本サイトはアフィリエイトリンクを含みます。リンク経由のご購入・お申し込みで運営者が紹介料を受け取る場合があります。
Invoker Commands は、button に commandfor(操作する要素の id)と command(何をするか)を書くだけで、ダイアログやポップオーバーを操作できる HTML の仕組みです。MDN では command の値として、ダイアログをモーダルで開く show-modal、閉じる close、ポップオーバーを開閉する toggle-popover などが挙げられ、close をボタンの value と一緒に使うとその値がダイアログの returnValue になると説明されています。-- で始まる独自の値を書くと、操作される側の要素に CommandEvent が届きます。
当サイトの Popover API Menu は popover 属性と popovertarget で JavaScript なしに開閉するポップオーバー、Modal Fade はオーバーレイがフェードしダイアログがスケールインするモーダル、Command Palette は ⌘K で開いて候補を絞り込むコマンドパレットです。本ページはダイアログを開閉するボタン側の書き方を HTML だけで済ませる別テーマです。
これまで dialog.showModal() を呼ぶだけの小さな JavaScript をボタンごとに書いていた場面で、その処理を属性に置き換えられます。操作の対象が属性に書かれているのでマークアップを読むだけで関係が分かり、JavaScript の読み込み前でもボタンが動くのも利点です。
どのブラウザで使えますか?
MDN の互換性データ(browser-compat-data 2026-09-24 版)では、button の command と commandfor は Chrome・Edge 135、Firefox 144、Safari 26.2 から使えます。本実装は 'command' in HTMLButtonElement.prototype で対応を調べ、未対応のブラウザだけ同じ属性を読んでクリックで開閉する代わりの処理を動かしています。HTML の書き方は対応・未対応で変わりません。
-- で始まる命令は何に使いますか?
command に -- で始まる独自の値を書くと、ボタンを押した時に commandfor で指した要素へ command イベントが届きます。イベントの command プロパティに値が入っているので、1つのリスナーで「--flip」「--reset」のように複数の命令を振り分けられます。ボタンごとに click のリスナーを付ける代わりに、操作される側の要素にまとめて書けるのが利点です。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。