空のカート

空のカートイラストと車輪が回転するアイドルアニメーションを添えた空状態。CTAを押すとカートが弾み、買い物へ促す導線をやわらかく示します。

#css#svg#empty

ライブデモ

コード

HTML
<!-- 空状態: 空のカート。カートが軽く揺れ、車輪が回転する。CTAクリックで一度だけ弾む -->
<main class="ec-card">
  <svg class="ec-illus" viewBox="0 0 200 170" width="168" height="143" role="img" aria-label="商品が入っていない空のショッピングカートのイラスト">
    <g class="ec-sparkle" aria-hidden="true">
      <path d="M150 26 L153 34 L161 37 L153 40 L150 48 L147 40 L139 37 L147 34 Z"></path>
    </g>
    <g class="ec-float" data-ec-float>
      <path class="ec-handle" d="M20 34 H34 L46 60"></path>
      <path class="ec-basket" d="M46 60 H156 L142 122 H62 Z"></path>
      <path class="ec-lattice" d="M80 60 L72 122 M122 60 L130 122"></path>
      <g class="ec-wheel-group ec-wheel-group--l">
        <circle class="ec-wheel" cx="76" cy="140" r="10"></circle>
        <line class="ec-spoke" x1="76" y1="131" x2="76" y2="149"></line>
        <line class="ec-spoke" x1="67" y1="140" x2="85" y2="140"></line>
      </g>
      <g class="ec-wheel-group ec-wheel-group--r">
        <circle class="ec-wheel" cx="128" cy="140" r="10"></circle>
        <line class="ec-spoke" x1="128" y1="131" x2="128" y2="149"></line>
        <line class="ec-spoke" x1="119" y1="140" x2="137" y2="140"></line>
      </g>
    </g>
  </svg>

  <h2 class="ec-title">カートは空です</h2>
  <p class="ec-desc">気になる商品はまだ何も追加されていません。お気に入りのアイテムを見つけて、カートに入れてみましょう。</p>

  <button class="ec-cta" type="button" data-cta data-label-default="買い物を続ける" data-label-busy="商品を探しています…">買い物を続ける</button>
</main>
CSS
/* 空状態: 空のカート */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 32px 20px;
  font-family: "Hiragino Sans", "Yu Gothic UI", "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 16% 16%, rgba(251, 146, 60, .16) 0%, transparent 46%),
    radial-gradient(circle at 86% 84%, rgba(253, 186, 116, .22) 0%, transparent 50%),
    #fff7ed;
  color: #7c2d12;
}

.ec-card {
  width: 100%;
  max-width: 380px;
  background: #ffffff;
  border-radius: 24px;
  padding: 40px 32px 34px;
  text-align: center;
  box-shadow: 0 24px 60px -24px rgba(234, 88, 12, .26), 0 2px 8px rgba(124, 45, 18, .06);
  border: 1px solid rgba(251, 146, 60, .14);
}

.ec-illus { display: block; margin: 0 auto 8px; overflow: visible; }

/* カート全体がゆっくり揺れる */
.ec-float {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: ec-sway 3.6s ease-in-out infinite;
}
@keyframes ec-sway {
  0%, 100% { transform: rotate(-2deg); }
  50% { transform: rotate(2deg); }
}
/* 一回だけ弾む(クリック時) */
.ec-float.is-bumped { animation: ec-bump .55s cubic-bezier(.34,1.56,.64,1); }
@keyframes ec-bump {
  0% { transform: translateY(0) rotate(-2deg); }
  40% { transform: translateY(-10px) rotate(2deg); }
  100% { transform: translateY(0) rotate(-2deg); }
}

.ec-basket { fill: rgba(251, 146, 60, .1); stroke: #ea580c; stroke-width: 6; stroke-linejoin: round; }
.ec-lattice { stroke: #fb923c; stroke-width: 3; opacity: .8; }
.ec-handle { fill: none; stroke: #ea580c; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }

.ec-wheel { fill: #ffffff; stroke: #9a3412; stroke-width: 4; }
.ec-spoke { stroke: #9a3412; stroke-width: 2.5; }
.ec-wheel-group { transform-box: fill-box; transform-origin: center; animation: ec-spin 5.5s linear infinite; }
.ec-wheel-group--r { animation-duration: 5.5s; }
@keyframes ec-spin { to { transform: rotate(360deg); } }

/* きらめき */
.ec-sparkle path { fill: #fbbf24; animation: ec-twinkle 2.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
@keyframes ec-twinkle {
  0%, 100% { opacity: .25; transform: scale(.75); }
  50% { opacity: 1; transform: scale(1.05); }
}

.ec-title { margin: 6px 0 10px; font-size: 21px; font-weight: 700; color: #7c2d12; }
.ec-desc { margin: 0 0 24px; font-size: 14.5px; line-height: 1.7; color: #9a6a4c; }

.ec-cta {
  appearance: none;
  border: none;
  cursor: pointer;
  min-width: 200px;
  padding: 13px 30px;
  font-size: 15px;
  font-weight: 600;
  color: #fff;
  background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
  border-radius: 999px;
  box-shadow: 0 12px 26px -10px rgba(234, 88, 12, .55);
  transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s ease, opacity .2s ease;
}
.ec-cta:hover { transform: translateY(-2px); box-shadow: 0 16px 32px -10px rgba(234, 88, 12, .65); }
.ec-cta:active { transform: translateY(0); }
.ec-cta:disabled { opacity: .75; cursor: default; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .ec-float, .ec-wheel-group, .ec-sparkle path { animation: none !important; }
}
JavaScript
// 空状態: 空のカート — CTAクリックでカートが一度弾み、ボタンが一時的に読み込み中の文言になる
(() => {
  const cta = document.querySelector('[data-cta]');
  const float = document.querySelector('[data-ec-float]');
  if (!cta) return; // null安全

  let busy = false;
  let resetTimer = null;

  cta.addEventListener('click', () => {
    if (busy) return;
    busy = true;

    const defaultLabel = cta.dataset.labelDefault || cta.textContent;
    const busyLabel = cta.dataset.labelBusy || defaultLabel;

    cta.disabled = true;
    cta.textContent = busyLabel;

    if (float) {
      float.classList.remove('is-bumped');
      // 強制リフローしてアニメーションを再始動できるようにする
      void float.offsetWidth;
      float.classList.add('is-bumped');
    }

    if (resetTimer) clearTimeout(resetTimer);
    resetTimer = setTimeout(() => {
      cta.disabled = false;
      cta.textContent = defaultLabel;
      busy = false;
    }, 900);
  });

  if (float) {
    float.addEventListener('animationend', (e) => {
      if (e.animationName === 'ec-bump') float.classList.remove('is-bumped');
    });
  }
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「空のカート」の効果を追加してください。

# 追加してほしい効果
空のカート(アラート & 空状態)
空のカートイラストと車輪が回転するアイドルアニメーションを添えた空状態。CTAを押すとカートが弾み、買い物へ促す導線をやわらかく示します。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 空状態: 空のカート。カートが軽く揺れ、車輪が回転する。CTAクリックで一度だけ弾む -->
<main class="ec-card">
  <svg class="ec-illus" viewBox="0 0 200 170" width="168" height="143" role="img" aria-label="商品が入っていない空のショッピングカートのイラスト">
    <g class="ec-sparkle" aria-hidden="true">
      <path d="M150 26 L153 34 L161 37 L153 40 L150 48 L147 40 L139 37 L147 34 Z"></path>
    </g>
    <g class="ec-float" data-ec-float>
      <path class="ec-handle" d="M20 34 H34 L46 60"></path>
      <path class="ec-basket" d="M46 60 H156 L142 122 H62 Z"></path>
      <path class="ec-lattice" d="M80 60 L72 122 M122 60 L130 122"></path>
      <g class="ec-wheel-group ec-wheel-group--l">
        <circle class="ec-wheel" cx="76" cy="140" r="10"></circle>
        <line class="ec-spoke" x1="76" y1="131" x2="76" y2="149"></line>
        <line class="ec-spoke" x1="67" y1="140" x2="85" y2="140"></line>
      </g>
      <g class="ec-wheel-group ec-wheel-group--r">
        <circle class="ec-wheel" cx="128" cy="140" r="10"></circle>
        <line class="ec-spoke" x1="128" y1="131" x2="128" y2="149"></line>
        <line class="ec-spoke" x1="119" y1="140" x2="137" y2="140"></line>
      </g>
    </g>
  </svg>

  <h2 class="ec-title">カートは空です</h2>
  <p class="ec-desc">気になる商品はまだ何も追加されていません。お気に入りのアイテムを見つけて、カートに入れてみましょう。</p>

  <button class="ec-cta" type="button" data-cta data-label-default="買い物を続ける" data-label-busy="商品を探しています…">買い物を続ける</button>
</main>

【CSS】
/* 空状態: 空のカート */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 32px 20px;
  font-family: "Hiragino Sans", "Yu Gothic UI", "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 16% 16%, rgba(251, 146, 60, .16) 0%, transparent 46%),
    radial-gradient(circle at 86% 84%, rgba(253, 186, 116, .22) 0%, transparent 50%),
    #fff7ed;
  color: #7c2d12;
}

.ec-card {
  width: 100%;
  max-width: 380px;
  background: #ffffff;
  border-radius: 24px;
  padding: 40px 32px 34px;
  text-align: center;
  box-shadow: 0 24px 60px -24px rgba(234, 88, 12, .26), 0 2px 8px rgba(124, 45, 18, .06);
  border: 1px solid rgba(251, 146, 60, .14);
}

.ec-illus { display: block; margin: 0 auto 8px; overflow: visible; }

/* カート全体がゆっくり揺れる */
.ec-float {
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: ec-sway 3.6s ease-in-out infinite;
}
@keyframes ec-sway {
  0%, 100% { transform: rotate(-2deg); }
  50% { transform: rotate(2deg); }
}
/* 一回だけ弾む(クリック時) */
.ec-float.is-bumped { animation: ec-bump .55s cubic-bezier(.34,1.56,.64,1); }
@keyframes ec-bump {
  0% { transform: translateY(0) rotate(-2deg); }
  40% { transform: translateY(-10px) rotate(2deg); }
  100% { transform: translateY(0) rotate(-2deg); }
}

.ec-basket { fill: rgba(251, 146, 60, .1); stroke: #ea580c; stroke-width: 6; stroke-linejoin: round; }
.ec-lattice { stroke: #fb923c; stroke-width: 3; opacity: .8; }
.ec-handle { fill: none; stroke: #ea580c; stroke-width: 7; stroke-linecap: round; stroke-linejoin: round; }

.ec-wheel { fill: #ffffff; stroke: #9a3412; stroke-width: 4; }
.ec-spoke { stroke: #9a3412; stroke-width: 2.5; }
.ec-wheel-group { transform-box: fill-box; transform-origin: center; animation: ec-spin 5.5s linear infinite; }
.ec-wheel-group--r { animation-duration: 5.5s; }
@keyframes ec-spin { to { transform: rotate(360deg); } }

/* きらめき */
.ec-sparkle path { fill: #fbbf24; animation: ec-twinkle 2.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
@keyframes ec-twinkle {
  0%, 100% { opacity: .25; transform: scale(.75); }
  50% { opacity: 1; transform: scale(1.05); }
}

.ec-title { margin: 6px 0 10px; font-size: 21px; font-weight: 700; color: #7c2d12; }
.ec-desc { margin: 0 0 24px; font-size: 14.5px; line-height: 1.7; color: #9a6a4c; }

.ec-cta {
  appearance: none;
  border: none;
  cursor: pointer;
  min-width: 200px;
  padding: 13px 30px;
  font-size: 15px;
  font-weight: 600;
  color: #fff;
  background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
  border-radius: 999px;
  box-shadow: 0 12px 26px -10px rgba(234, 88, 12, .55);
  transition: transform .25s cubic-bezier(.22,1,.36,1), box-shadow .25s ease, opacity .2s ease;
}
.ec-cta:hover { transform: translateY(-2px); box-shadow: 0 16px 32px -10px rgba(234, 88, 12, .65); }
.ec-cta:active { transform: translateY(0); }
.ec-cta:disabled { opacity: .75; cursor: default; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .ec-float, .ec-wheel-group, .ec-sparkle path { animation: none !important; }
}

【JavaScript】
// 空状態: 空のカート — CTAクリックでカートが一度弾み、ボタンが一時的に読み込み中の文言になる
(() => {
  const cta = document.querySelector('[data-cta]');
  const float = document.querySelector('[data-ec-float]');
  if (!cta) return; // null安全

  let busy = false;
  let resetTimer = null;

  cta.addEventListener('click', () => {
    if (busy) return;
    busy = true;

    const defaultLabel = cta.dataset.labelDefault || cta.textContent;
    const busyLabel = cta.dataset.labelBusy || defaultLabel;

    cta.disabled = true;
    cta.textContent = busyLabel;

    if (float) {
      float.classList.remove('is-bumped');
      // 強制リフローしてアニメーションを再始動できるようにする
      void float.offsetWidth;
      float.classList.add('is-bumped');
    }

    if (resetTimer) clearTimeout(resetTimer);
    resetTimer = setTimeout(() => {
      cta.disabled = false;
      cta.textContent = defaultLabel;
      busy = false;
    }, 900);
  });

  if (float) {
    float.addEventListener('animationend', (e) => {
      if (e.animationName === 'ec-bump') float.classList.remove('is-bumped');
    });
  }
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。