EC サイトで「カートに入れる」と「今すぐ購入」を分けるべき理由と実装
はじめに
EC サイトを作るとき、商品ページに「購入ボタン」を置こうとして、
「カートに追加するのか、直接購入なのか」で迷ったことはありませんか?
これは UX 設計上で明確に分けるべき概念です。
混同すると、ユーザーが「あれ、買ってしまった?カートに入れただけ?」と混乱します。
完成イメージ
実際のアプリでは、作品詳細に「今すぐレンタル」と「カートへ追加」の 2 ボタンを配置しています:
「カートへ追加」を押すとサイドドロワーが開きます:
「今すぐレンタル」を押すと直接決済画面へ遷移します:
2つのボタンが解決する問題
ユーザーの2種類の購買行動
行動A:まとめて購入したい
「この映画とあの映画と、もう1本。3本まとめてレンタルしたい」
→ カートに追加 → カートページで確認 → まとめて決済
行動B:今すぐこれだけ買いたい
「この映画が気に入った。すぐ見たい」
→ 商品ページから直接決済画面へ → 即座に完了
この2つを同じボタンで処理しようとすると、どちらかの UX が犠牲になります。
ボタンの役割の違い
| ボタン | 役割 | 画面遷移先 |
|---|---|---|
| カートに入れる | 商品をカートに追加する | カートドロワー or カートページ |
| 今すぐ購入 | 直接決済フローへ進む | 決済画面(チェックアウト) |
Vue 3 での実装例
<!-- FilmDetailModal.vue -->
<template>
<div class="film-actions">
<!-- カートに入れる: カートへ追加してドロワーを開く -->
<button
class="btn btn-outline-primary"
@click="addToCart"
:disabled="isInCart"
>
<i class="bi bi-cart-plus" />
{{ isInCart ? 'カートに追加済み' : 'カートに入れる' }}
</button>
<!-- 今すぐ購入: 直接チェックアウトへ -->
<button
class="btn btn-primary"
@click="buyNow"
>
<i class="bi bi-lightning-fill" />
今すぐ購入
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useCartStore } from '@/stores/cartStore'
import { useRouter } from 'vue-router'
interface Props {
filmId: number
title: string
rentalRate: number
}
const props = defineProps<Props>()
const cartStore = useCartStore()
const router = useRouter()
const isInCart = computed(() =>
cartStore.items.some(item => item.filmId === props.filmId)
)
// カートに入れる
function addToCart() {
cartStore.addItem({
filmId: props.filmId,
title: props.title,
rentalRate: props.rentalRate,
})
cartStore.openDrawer() // サイドドロワーを開いてカートを見せる
}
// 今すぐ購入: このアイテムだけでチェックアウトへ
function buyNow() {
router.push({
path: '/checkout',
query: {
mode: 'buy-now',
filmId: props.filmId,
},
})
}
</script>
チェックアウト画面での「今すぐ購入」モードの処理
/checkout?mode=buy-now&filmId=123 でアクセスされた場合、
カートの内容ではなく、指定された商品だけを決済対象にします。
// CheckoutView.vue
import { useRoute } from 'vue-router'
import { useCartStore } from '@/stores/cartStore'
import { ref, onMounted } from 'vue'
const route = useRoute()
const cartStore = useCartStore()
// 決済対象アイテム
const checkoutItems = ref([])
onMounted(async () => {
if (route.query.mode === 'buy-now' && route.query.filmId) {
// 今すぐ購入: このアイテムだけ
const film = await fetchFilm(Number(route.query.filmId))
checkoutItems.value = [film]
} else {
// 通常フロー: カートの内容
checkoutItems.value = cartStore.items
}
})
カートドロワー UI
「カートに入れる」を押したとき、画面を遷移させずにサイドからドロワーが開くのが良い UX です。
ユーザーが「カート確認→買い物続行」か「カート確認→決済へ」を選べるようになります。
<!-- CartDrawer.vue -->
<template>
<Transition name="slide-right">
<div v-if="cartStore.isDrawerOpen" class="cart-drawer">
<div class="cart-drawer-header">
<h5>カート ({{ cartStore.totalItems }}件)</h5>
<button @click="cartStore.closeDrawer()">✕</button>
</div>
<div class="cart-drawer-body">
<CartItem
v-for="item in cartStore.items"
:key="item.filmId"
:item="item"
/>
</div>
<div class="cart-drawer-footer">
<div class="total">合計: ¥{{ cartStore.totalAmount }}</div>
<RouterLink to="/checkout" @click="cartStore.closeDrawer()">
<button class="btn btn-primary w-100">
レジに進む
</button>
</RouterLink>
</div>
</div>
</Transition>
<!-- オーバーレイ -->
<div
v-if="cartStore.isDrawerOpen"
class="cart-overlay"
@click="cartStore.closeDrawer()"
/>
</template>
よくある間違い
❌「カートに入れる」ボタンを押したらすぐ決済ページへ遷移させる
カートの概念がない設計になります。
「複数商品をまとめて買いたい」ユーザーが困ります。
❌「今すぐ購入」がカートに入れてから決済ページに遷移させる
カートが汚染されます。
「今すぐ購入」後にカートを開くと、買い終わった商品が残ってしまいます。
❌ ボタンが1つで「カートに入れる or 購入」を切り替えるデザイン
ユーザーが自分の意図を選ぶ手順が増えます。2つのボタンを別々に配置する方が直感的です。
まとめ
| カートに入れる | 今すぐ購入 | |
|---|---|---|
| 目的 | 複数商品をまとめて購入する準備 | 1商品をすぐに購入 |
| 遷移先 | カートドロワー / カートページ | 決済画面(チェックアウト) |
| カートへの影響 | カートに追加する | カートには追加しない |
| 処理後の行動 | 引き続き商品を探せる | 決済完了まで決済フローに集中 |
Amazon や楽天が「カートに追加」と「今すぐ買う」を分けているのには、こういった UX 設計の根拠があります。
このアプリでの実装
このDVDレンタルアプリでは、Vue Router や Pinia ではなく、emit イベントと currentPage ref によって実現しています。
作品詳細の CTAボタン(実際のコード)
<!-- FilmDetailView.vue -->
<div class="cta-row">
<!-- レンタル区分のセクションかどうかで文言を変える -->
<template v-if="isRentalSection">
<button class="btn-primary" @click="emit('direct-checkout', film)">今すぐレンタル(決済)</button>
<button v-if="isAuthenticated" class="btn-secondary" @click="emit('add-to-cart', film)">カートへ追加</button>
</template>
<template v-else>
<button class="btn-primary" @click="emit('direct-checkout', film)">今すぐ購入する(決済)</button>
<button v-if="isAuthenticated" class="btn-secondary" @click="emit('add-to-cart', film)">カートへ追加</button>
</template>
<button class="btn-secondary" @click="emit('add-watch-later', film)">お気に入りに追加</button>
</div>
emit の型定義:
const emit = defineEmits<{
(e: 'back'): void
(e: 'direct-checkout', film: PublicFilmSummary): void
(e: 'add-to-cart', film: PublicFilmSummary): void
(e: 'add-watch-later', film: PublicFilmSummary): void
}>()
App.vue でのハンドリング(実際のコード)
// App.vue(emit受け取り側)
// カートに入れる:useCart composable 経由でlocalStorageに保存
function handleAddToCart(film: PublicFilmSummary) {
const added = addToCart(film)
if (added) {
cartToastMessage.value = `「${film.title}」をカートに追加しました`
cartToastTimer = setTimeout(() => { cartToastMessage.value = '' }, 2000)
}
}
// 今すぐ購入:currentPage を切り替えて決済画面へ遷移
const handleDirectCheckout = (film: PublicFilmSummary) => {
selectedFilm.value = film
currentPage.value = 'checkout'
}
カートはルーターやグローバルストアを使わず、useCart composable がシングルトンとして localStorage の永続化を担当。
画面遷移も currentPage という Union Literal 型の ref を書き換えるだけで完結します。


