Why an EC Site Should Separate 'Add to Cart' and 'Buy Now', and How to Implement It

Vue3 UX TypeScript Frontend

Introduction

When building an EC site, have you ever hesitated over whether a product page’s purchase button should
add to the cart, or go straight to purchase?

This is a concept that should be clearly separated at the UX design level.
Mix them up, and users get confused: “Wait, did I just buy it, or did I just add it to the cart?”


The Problem the Two Buttons Solve

Two Types of User Purchasing Behavior

Behavior A: I want to buy several things together

“This movie, and that movie, and one more. I want to rent all three together.”

→ Add to cart → Review on the cart page → Pay for everything at once

Behavior B: I want to buy just this one, right now

“I like this movie. I want to watch it right away.”

→ Straight from the product page to checkout → Done immediately

If you try to handle both with a single button, one of the two experiences gets sacrificed.


The Difference in Button Roles

ButtonRoleNavigates to
Add to CartAdds the item to the cartCart drawer or cart page
Buy NowGoes straight into the checkout flowCheckout screen

Implementation Example in Vue 3

<!-- FilmDetailModal.vue -->
<template>
  <div class="film-actions">
    <!-- Add to Cart: adds to the cart and opens the drawer -->
    <button
      class="btn btn-outline-primary"
      @click="addToCart"
      :disabled="isInCart"
    >
      <i class="bi bi-cart-plus" />
      {{ isInCart ? 'Already in Cart' : 'Add to Cart' }}
    </button>

    <!-- Buy Now: goes straight to checkout -->
    <button
      class="btn btn-primary"
      @click="buyNow"
    >
      <i class="bi bi-lightning-fill" />
      Buy Now
    </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)
)

// Add to cart
function addToCart() {
  cartStore.addItem({
    filmId: props.filmId,
    title: props.title,
    rentalRate: props.rentalRate,
  })
  cartStore.openDrawer()  // open the side drawer to show the cart
}

// Buy Now: go to checkout with just this item
function buyNow() {
  router.push({
    path: '/checkout',
    query: {
      mode: 'buy-now',
      filmId: props.filmId,
    },
  })
}
</script>

Handling “Buy Now” Mode on the Checkout Screen

When accessed as /checkout?mode=buy-now&filmId=123,
the checkout target is just the specified item — not the cart’s contents.

// CheckoutView.vue
import { useRoute } from 'vue-router'
import { useCartStore } from '@/stores/cartStore'
import { ref, onMounted } from 'vue'

const route = useRoute()
const cartStore = useCartStore()

// Items to check out
const checkoutItems = ref([])

onMounted(async () => {
  if (route.query.mode === 'buy-now' && route.query.filmId) {
    // Buy Now: just this item
    const film = await fetchFilm(Number(route.query.filmId))
    checkoutItems.value = [film]
  } else {
    // Normal flow: the cart's contents
    checkoutItems.value = cartStore.items
  }
})

Cart Drawer UI

When “Add to Cart” is pressed, opening a drawer from the side without navigating away is good UX.
It lets the user choose between “check the cart → keep shopping” or “check the cart → go to checkout.”

<!-- CartDrawer.vue -->
<template>
  <Transition name="slide-right">
    <div v-if="cartStore.isDrawerOpen" class="cart-drawer">
      <div class="cart-drawer-header">
        <h5>Cart ({{ cartStore.totalItems }} items)</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">Total: ¥{{ cartStore.totalAmount }}</div>
        <RouterLink to="/checkout" @click="cartStore.closeDrawer()">
          <button class="btn btn-primary w-100">
            Proceed to Checkout
          </button>
        </RouterLink>
      </div>
    </div>
  </Transition>
  
  <!-- overlay -->
  <div
    v-if="cartStore.isDrawerOpen"
    class="cart-overlay"
    @click="cartStore.closeDrawer()"
  />
</template>

Common Mistakes

❌ Navigating Straight to Checkout When “Add to Cart” Is Pressed

This ends up as a design with no real concept of a cart.
Users who want to buy multiple items together are left stuck.

❌ “Buy Now” Adding to the Cart Before Navigating to Checkout

This pollutes the cart.
Open the cart after “Buy Now,” and the item you already bought is still sitting there.

❌ A Single Button That Toggles Between “Add to Cart” and “Buy”

This adds a step for the user to select their intent. Placing two separate buttons is more intuitive.


Summary

Add to CartBuy Now
PurposePreparing to buy multiple items togetherBuying one item right away
Navigates toCart drawer / cart pageCheckout screen
Effect on cartAdds to the cartDoesn’t touch the cart
Behavior afterwardCan keep browsingStays focused on checkout until payment completes

Amazon and Rakuten separating “Add to Cart” from “Buy Now” comes from exactly this kind of UX design reasoning.


Implementation in This App

This DVD rental app implements it not with Vue Router or Pinia, but with emit events and a currentPage ref.

CTA Buttons on the Film Detail Page (Actual Code)

<!-- FilmDetailView.vue -->
<div class="cta-row">
  <!-- The wording changes depending on whether this is the rental section -->
  <template v-if="isRentalSection">
    <button class="btn-primary" @click="emit('direct-checkout', film)">Rent Now (Checkout)</button>
    <button v-if="isAuthenticated" class="btn-secondary" @click="emit('add-to-cart', film)">Add to Cart</button>
  </template>
  <template v-else>
    <button class="btn-primary" @click="emit('direct-checkout', film)">Buy Now (Checkout)</button>
    <button v-if="isAuthenticated" class="btn-secondary" @click="emit('add-to-cart', film)">Add to Cart</button>
  </template>

  <button class="btn-secondary" @click="emit('add-watch-later', film)">Add to Favorites</button>
</div>

The emit type definition:

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
}>()

Handling in App.vue (Actual Code)

// App.vue (the emit receiver)

// Add to cart: persisted to localStorage via the useCart composable
function handleAddToCart(film: PublicFilmSummary) {
  const added = addToCart(film)
  if (added) {
    cartToastMessage.value = `"${film.title}" was added to the cart`
    cartToastTimer = setTimeout(() => { cartToastMessage.value = '' }, 2000)
  }
}

// Buy Now: switch currentPage to navigate to checkout
const handleDirectCheckout = (film: PublicFilmSummary) => {
  selectedFilm.value = film
  currentPage.value = 'checkout'
}

The cart doesn’t use a router or a global store — the useCart composable, as a singleton, handles persistence to localStorage. Navigation, too, is done just by rewriting the currentPage ref, a union literal type.

Actual Screens

The film detail CTA buttons (“Rent Now” and “Add to Cart”):

Film detail CTA buttons

The cart drawer after “Add to Cart”:

Cart drawer

The checkout screen reached via “Rent Now”:

Checkout screen


Article Map for This Series

Building an End-User DVD Rental App — Vue 3 + Spring Boot Paired with the Admin App, with Article Map

Feel free to send a message

Job offers, project referrals, feedback, questions — anything is welcome. I sincerely hope to connect with people who share high ambitions. I will keep taking on the challenges I have staked my life on. Thank you very much.