AI先生のロボットキャラクター
第1章 - セクション7

承認ボタン(印鑑風アニメーション)

クリックで印鑑スタンプに変化する、承認フローに最適なインタラクティブボタン

男子生徒のアイコン

承認フローってボタンが押せなくなった後も見た目が地味なままで、承認したかどうかわかりにくいですよね。

AI先生のアイコン

そうだね。今回は、クリックすると赤い印鑑スタンプに変化するボタンを作ってみたんだ。承認したことが一目でわかって、押した満足感もあるよ。

女子生徒のアイコン

印鑑風って面白い!どんな仕組みなんですか?

AI先生のアイコン

SVGフィルターを使って、印鑑のかすれやにじみを表現しているんだ。ホバー時には立体感が出て、クリック後は赤い印鑑スタンプとして固定されるよ。早速見てみよう。

作成した承認ボタンパーツ

グレーの丸ボタンが、クリックすると赤い印鑑スタンプに変化する承認ボタンです。SVGフィルターで印鑑特有の質感を再現し、ホバー時には立体感のある視覚効果も加えています。

印鑑風承認ボタン

クリックすると承認済みの印鑑スタンプに変化する、インタラクティブなボタンです。

HTML
<svg class="filter-def">
    <defs>
        <filter id="roughStamp" x="-50%" y="-50%" width="200%" height="200%">
            <feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="3" seed="17" result="noise1"></feTurbulence>
            <feTurbulence type="fractalNoise" baseFrequency="6" numOctaves="2" seed="23" result="noise2"></feTurbulence>
            <feBlend in="noise1" in2="noise2" mode="multiply" result="combined"></feBlend>
            <feColorMatrix in="combined" type="matrix" values="1 0 0 0 0  1 0 0 0 0  1 0 0 0 0  0 0 0 35 -14" result="threshold"></feColorMatrix>
            <feGaussianBlur in="threshold" stdDeviation="1.5" result="smoothed"></feGaussianBlur>
            <feTurbulence type="fractalNoise" baseFrequency="1.2" numOctaves="2" seed="5" result="warp"></feTurbulence>
            <feDisplacementMap in="SourceGraphic" in2="warp" scale="1.5" xChannelSelector="R" yChannelSelector="G" result="warped"></feDisplacementMap>
            <feComposite in="warped" in2="smoothed" operator="in" result="final"></feComposite>
        </filter>
        <filter id="inkBleed" x="-50%" y="-50%" width="200%" height="200%">
            <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="3" seed="29" result="noise1"></feTurbulence>
            <feTurbulence type="fractalNoise" baseFrequency="7" numOctaves="2" seed="31" result="noise2"></feTurbulence>
            <feBlend in="noise1" in2="noise2" mode="multiply" result="combined"></feBlend>
            <feColorMatrix in="combined" type="matrix" values="1 0 0 0 0  1 0 0 0 0  1 0 0 0 0  0 0 0 33 -13" result="threshold"></feColorMatrix>
            <feGaussianBlur in="threshold" stdDeviation="1.5" result="smoothed"></feGaussianBlur>
            <feComposite in="SourceGraphic" in2="smoothed" operator="in"></feComposite>
        </filter>
        <filter id="textRough" x="-50%" y="-50%" width="200%" height="200%">
            <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="3" seed="41" result="noise1"></feTurbulence>
            <feTurbulence type="fractalNoise" baseFrequency="7" numOctaves="2" seed="47" result="noise2"></feTurbulence>
            <feBlend in="noise1" in2="noise2" mode="multiply" result="combined"></feBlend>
            <feColorMatrix in="combined" type="matrix" values="1 0 0 0 0  1 0 0 0 0  1 0 0 0 0  0 0 0 35 -13" result="threshold"></feColorMatrix>
            <feGaussianBlur in="threshold" stdDeviation="1.4" result="smoothed"></feGaussianBlur>
            <feTurbulence type="fractalNoise" baseFrequency="1.2" numOctaves="2" seed="13" result="warp"></feTurbulence>
            <feDisplacementMap in="SourceGraphic" in2="warp" scale="1.3" xChannelSelector="R" yChannelSelector="G" result="warped"></feDisplacementMap>
            <feComposite in="warped" in2="smoothed" operator="in"></feComposite>
        </filter>
    </defs>
</svg>
<button class="btn" type="button" id="stampBtn" aria-pressed="false">承認</button>
CSS
:root {
    --ink: #d32f2f;
    --btn: #141821;
}

.btn {
    appearance: none;
    border: none;
    background: #d6d6d6;
    width: 110px;
    height: 110px;
    border-radius: 999px;
    font-size: 24px;
    font-weight: 900;
    letter-spacing: 0.1em;
    color: rgba(0, 0, 0, 0.5);
    cursor: pointer;
    transition: all 280ms cubic-bezier(0.16, 1, 0.3, 1);
    text-shadow: none;
    box-shadow: none;
    border-bottom: none;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    position: relative;
    overflow: visible;
}

.btn:focus-visible {
    outline: 3px solid rgba(211, 47, 47, 0.4);
    outline-offset: 4px;
}

.btn::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 110px;
    height: 110px;
    transform: translate(-50%, -50%) scale(0.96) rotate(-1deg);
    border-radius: 999px;
    border: 5px solid rgba(0, 0, 0, 0.15);
    opacity: 0;
    transition: opacity 280ms cubic-bezier(0.16, 1, 0.3, 1),
                transform 280ms cubic-bezier(0.16, 1, 0.3, 1),
                border-color 280ms cubic-bezier(0.16, 1, 0.3, 1),
                filter 280ms cubic-bezier(0.16, 1, 0.3, 1);
    pointer-events: none;
}

.btn:hover:not(.is-stamped) {
    background-image: linear-gradient(#e8e8e8 0%, #d6d6d6 100%);
    color: rgba(0, 0, 0, 0.4);
    text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.66);
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.5),
                0 2px 2px rgba(0, 0, 0, 0.19);
    border-bottom: solid 2px #b5b5b5;
}

.btn:active {
    background: #d6d6d6 !important;
    color: rgba(0, 0, 0, 0.4) !important;
    text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.66) !important;
    box-shadow: none !important;
    border-bottom: none !important;
    transform: scale(0.98) translateY(1px) !important;
}

.btn:active::after {
    opacity: 0;
}

.btn.is-stamped {
    background: transparent;
    border: none;
    border-bottom: none;
    color: var(--ink);
    cursor: default;
    box-shadow: none;
    text-shadow: none;
    filter: url(#textRough);
}

.btn.is-stamped::after {
    opacity: 0.88;
    transform: translate(-50%, -50%) scale(1) rotate(-1deg);
    border-color: var(--ink);
    filter: url(#roughStamp);
    border-style: solid;
    border-width: 5px 4px 6px 4px;
}

@media (prefers-reduced-motion: reduce) {
    * {
        transition-duration: 0.01ms !important;
    }
}

.filter-def {
    position: absolute;
    width: 0;
    height: 0;
    pointer-events: none;
}
JavaScript
(() => {
    const btn = document.getElementById('stampBtn');
    if (!btn) return;
    
    btn.addEventListener('click', () => {
        if (btn.classList.contains('is-stamped')) return;
        btn.classList.add('is-stamped');
        btn.setAttribute('aria-pressed', 'true');
        btn.disabled = true;
    });
})();

AIへのプロンプト例

以下のようなプロンプトをAIに送信します:

button要素を使用して、クリックすると印鑑風のスタンプに変化する承認ボタンを作成してください。

### 初期状態
- グレーの丸いボタン
- 「承認」という文字を中央に配置
- 丸いボタンの形状

### ホバー時
- 立体感のあるグラデーション
- 光沢感のあるテキスト影
- 下部に境界線で押せる印象

### クリック時
- 赤い印鑑風のスタンプに変化
- 印影のようなかすれやにじみを表現
- テキストと枠線に質感を追加
- クリック後は固定され、再度押せない

### その他の要件
- SVGフィルターを使って印鑑特有の質感を表現
- アクセシビリティ対応(aria-pressed属性)
- アニメーション軽減設定に対応

このボタンの特徴

  • 印鑑スタンプ効果 SVGフィルターで本物の印鑑のようなかすれとにじみを再現
  • 状態の明確化 グレーから赤への変化で承認済みを視覚的に表現
  • 立体的なホバー効果 グラデーションと影で押せることを示唆
  • 再押下防止 クリック後はボタンを無効化し、誤操作を防止
  • アクセシビリティ aria-pressed属性で状態を明示

コードのポイント

SVGフィルターによる印鑑質感の表現
.btn.is-stamped::after {
    filter: url(#roughStamp);  /* 印鑑のかすれを表現 */
    border-color: var(--ink);
    opacity: 0.88;
}

.btn.is-stamped {
    filter: url(#textRough);  /* テキストのにじみを表現 */
    color: var(--ink);
}

SVGのfeTurbulencefeDisplacementMapを組み合わせて、印鑑特有のかすれやにじみを表現しています。

疑似要素による円形の枠線
.btn::after {
    content: '';
    position: absolute;
    width: 110px;
    height: 110px;
    border-radius: 999px;
    border: 5px solid rgba(0, 0, 0, 0.15);
    opacity: 0;  /* 初期状態では非表示 */
}

.btn.is-stamped::after {
    opacity: 0.88;  /* スタンプ時に表示 */
    transform: translate(-50%, -50%) scale(1) rotate(-1deg);
}

::after疑似要素でボタンの上に円形の枠線を配置し、クリック時に印鑑の外枠として表示します。

JavaScriptによる状態管理
btn.addEventListener('click', () => {
    if (btn.classList.contains('is-stamped')) return;  // 既に押されていたら何もしない
    btn.classList.add('is-stamped');  // スタンプ状態にする
    btn.setAttribute('aria-pressed', 'true');  // アクセシビリティ対応
    btn.disabled = true;  // ボタンを無効化
});

クリック時にis-stampedクラスを追加してスタンプ状態に変化させ、disabled属性で再押下を防止します。

女子生徒のアイコン

印鑑のかすれが本物みたいにリアルですね!

AI先生のアイコン

SVGフィルターの力だね。feTurbulenceでランダムなノイズを生成して、feDisplacementMapで歪ませることで、印鑑特有の質感を表現しているんだ。

男子生徒のアイコン

クリック後にボタンが無効化されるのもいいですね。承認の二重送信を防げます。

AI先生のアイコン

そうだね。disabled属性とaria-pressed属性を組み合わせることで、機能面でもアクセシビリティ面でも適切な実装になっているよ。

まとめ

男子生徒のアイコン

印鑑風のボタンって、承認フローにぴったりですね。視覚的にも分かりやすいし、押した感じも楽しいです。

女子生徒のアイコン

SVGフィルターでこんなリアルな質感が出せるなんて驚きました!他のデザインにも応用できそうです。

AI先生のアイコン

そうだね。SVGフィルターは、通常のCSSでは難しい質感表現を可能にしてくれる強力なツールなんだ。印鑑以外にも、手書き風のテキストや紙の質感など、様々な表現に活用できるよ。

  • SVGフィルター feTurbulencefeDisplacementMapで印鑑のかすれとにじみを再現
  • 疑似要素の活用 ::afterで印鑑の外枠を作り、クリック時に表示
  • 状態管理 is-stampedクラスで視覚的な変化を制御
  • 再押下防止 disabled属性で誤操作を防止
  • アクセシビリティ aria-pressed属性でスクリーンリーダーに状態を伝達

承認フローやワークフローUIに最適なインタラクティブボタンです。SVGフィルターを使った高度な視覚効果と、適切な状態管理を学びました。

学習チェック

このレッスンを理解できたら「完了」をクリックしてください。
後で見直したい場合は「未完了に戻す」で進捗をリセットできます。

レッスン完了!🎉

お疲れさまでした!