10.1k
New Components: Field, Input Group, Item and more

Building Blocks for the Web

Clean, modern building blocks. Copy and paste into your apps. Works with all Vue frameworks. Open Source. Free forever.

Files
components/CheckoutForm.vue
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { type PriceType, formatPrice } from '@/lib/format'

interface OrderItem {
  name: string
  variant: string
  price: PriceType
  quantity: number
  image?: string
}

const props = withDefaults(
  defineProps<{
    initialItems?: OrderItem[]
    currency?: string
  }>(),
  {
    initialItems: undefined,
    currency: 'EUR',
  },
)

const form = reactive({
  email: '',
  firstName: '',
  lastName: '',
  address: '',
  apartment: '',
  city: '',
  zip: '',
  country: 'DE',
  cardNumber: '',
  expiry: '',
  cvc: '',
})

const defaultItems: OrderItem[] = [
  { name: 'Classic Leather Sneakers', variant: 'White / 42', price: { amount: 12999, currency: props.currency }, quantity: 1 },
  { name: 'Organic Cotton T-Shirt', variant: 'Navy / M', price: { amount: 3999, currency: props.currency }, quantity: 2 },
]

const orderItems = computed(() => props.initialItems ?? defaultItems)

const orderSummary = computed(() => {
  const items = orderItems.value
  const subtotal = items.reduce((sum, item) => sum + item.price.amount * item.quantity, 0)
  const shipping = subtotal > 10000 ? 0 : 599
  return { items, subtotal, shipping, total: subtotal + shipping }
})
</script>

<template>
  <div>
    <h1 class="mb-6 text-2xl font-semibold">
      Checkout
    </h1>

    <div class="grid gap-6 md:grid-cols-[1fr_360px]">
      <!-- Form -->
      <div class="space-y-6">
        <!-- Contact -->
        <Card>
          <CardHeader>
            <CardTitle class="text-base">Contact</CardTitle>
          </CardHeader>
          <CardContent>
            <input
              v-model="form.email"
              type="email"
              placeholder="Email address"
              class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
            />
          </CardContent>
        </Card>

        <!-- Shipping Address -->
        <Card>
          <CardHeader>
            <CardTitle class="text-base">Shipping Address</CardTitle>
          </CardHeader>
          <CardContent class="space-y-3">
            <select
              v-model="form.country"
              class="h-10 w-full rounded-md border bg-background px-3 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
            >
              <option value="DE">Germany</option>
              <option value="AT">Austria</option>
              <option value="CH">Switzerland</option>
              <option value="GB">United Kingdom</option>
              <option value="US">United States</option>
            </select>
            <div class="grid grid-cols-2 gap-3">
              <input
                v-model="form.firstName"
                type="text"
                placeholder="First name"
                class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
              />
              <input
                v-model="form.lastName"
                type="text"
                placeholder="Last name"
                class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
              />
            </div>
            <input
              v-model="form.address"
              type="text"
              placeholder="Address"
              class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
            />
            <input
              v-model="form.apartment"
              type="text"
              placeholder="Apartment, suite, etc. (optional)"
              class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
            />
            <div class="grid grid-cols-2 gap-3">
              <input
                v-model="form.zip"
                type="text"
                placeholder="Postal code"
                class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
              />
              <input
                v-model="form.city"
                type="text"
                placeholder="City"
                class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
              />
            </div>
          </CardContent>
        </Card>

        <!-- Payment -->
        <Card>
          <CardHeader>
            <CardTitle class="text-base">Payment</CardTitle>
          </CardHeader>
          <CardContent class="space-y-3">
            <input
              v-model="form.cardNumber"
              type="text"
              placeholder="Card number"
              class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
            />
            <div class="grid grid-cols-2 gap-3">
              <input
                v-model="form.expiry"
                type="text"
                placeholder="MM / YY"
                class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
              />
              <input
                v-model="form.cvc"
                type="text"
                placeholder="CVC"
                class="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
              />
            </div>
          </CardContent>
        </Card>

        <Button class="w-full" size="lg" color="checkout">
          Place Order
        </Button>
      </div>

      <!-- Order Summary -->
      <Card class="h-fit">
        <CardHeader>
          <CardTitle class="text-base">Order Summary</CardTitle>
        </CardHeader>
        <CardContent class="space-y-4">
          <div
            v-for="item in orderSummary.items"
            :key="item.name"
            class="flex items-center gap-3"
          >
            <div class="relative size-12 shrink-0">
              <img
                v-if="item.image"
                :src="item.image"
                :alt="item.name"
                class="size-12 rounded-md border object-cover"
              />
              <div
                v-else
                class="flex size-12 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground"
              >
                {{ item.quantity }}×
              </div>
              <span
                v-if="item.image && item.quantity > 1"
                class="absolute -top-1.5 -right-1.5 flex size-5 items-center justify-center rounded-full bg-foreground text-[10px] font-medium text-background"
              >
                {{ item.quantity }}
              </span>
            </div>
            <div class="flex-1 text-sm">
              <p class="font-medium">{{ item.name }}</p>
              <p class="text-xs text-muted-foreground">{{ item.variant }}</p>
            </div>
            <span class="text-sm">{{ formatPrice({ amount: item.price.amount * item.quantity, currency }) }}</span>
          </div>

          <div class="space-y-2 border-t pt-3 text-sm">
            <div class="flex justify-between">
              <span class="text-muted-foreground">Subtotal</span>
              <span>{{ formatPrice({ amount: orderSummary.subtotal, currency }) }}</span>
            </div>
            <div class="flex justify-between">
              <span class="text-muted-foreground">Shipping</span>
              <span>{{ orderSummary.shipping === 0 ? 'Free' : formatPrice({ amount: orderSummary.shipping, currency }) }}</span>
            </div>
            <div class="flex justify-between border-t pt-2 text-base font-semibold">
              <span>Total</span>
              <span>{{ formatPrice({ amount: orderSummary.total, currency }) }}</span>
            </div>
          </div>
        </CardContent>
      </Card>
    </div>
  </div>
</template>
A checkout form with shipping address and payment details.
checkout-form
Files
components/OrderSummary.vue
<script setup lang="ts">
import { computed } from 'vue'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Price } from '@/components/ui/price'
import CartItemMinimal from '@/components/CartItemMinimal.vue'

interface SummaryItem {
  id: string
  name: string
  variant: string
  price: { amount: number; currency: string }
  quantity: number
  image: string
}

const props = withDefaults(
  defineProps<{
    /** Line items to display */
    items?: SummaryItem[]
    /** Currency code */
    currency?: string
    /** Shipping cost in cents. 0 = free. */
    shippingCents?: number
    /** Tax amount in cents */
    taxCents?: number
    /** Whether to show the tax row */
    showTax?: boolean
    /** Label for the shipping row when free */
    freeShippingLabel?: string
  }>(),
  {
    items: undefined,
    currency: 'EUR',
    shippingCents: 0,
    taxCents: 0,
    showTax: true,
    freeShippingLabel: 'Free',
  },
)

const defaultItems: SummaryItem[] = [
  {
    id: '1',
    name: 'Classic Leather Sneakers',
    variant: 'White / Size 42',
    price: { amount: 12999, currency: props.currency },
    quantity: 1,
    image: '/placeholder.svg',
  },
  {
    id: '2',
    name: 'Organic Cotton T-Shirt',
    variant: 'Navy / Size M',
    price: { amount: 3999, currency: props.currency },
    quantity: 2,
    image: '/placeholder.svg',
  },
  {
    id: '3',
    name: 'Slim Fit Chinos',
    variant: 'Khaki / Size 32',
    price: { amount: 7999, currency: props.currency },
    quantity: 1,
    image: '/placeholder.svg',
  },
]

const summaryItems = computed(() => props.items ?? defaultItems)

const subtotalCents = computed(() =>
  summaryItems.value.reduce((sum, item) => sum + item.price.amount * item.quantity, 0),
)

const totalCents = computed(
  () => subtotalCents.value + props.shippingCents + props.taxCents,
)
</script>

<template>
  <Card>
    <CardHeader>
      <CardTitle class="text-base">Order Summary</CardTitle>
    </CardHeader>
    <CardContent class="space-y-4">
      <!-- Line items — composed from cart-item-03 -->
      <CartItemMinimal :items="summaryItems" :currency="currency" />

      <Separator />

      <!-- Totals -->
      <div class="space-y-2 text-sm">
        <div class="flex justify-between">
          <span class="text-muted-foreground">Subtotal</span>
          <Price :price="{ amount: subtotalCents, currency }" />
        </div>
        <div class="flex justify-between">
          <span class="text-muted-foreground">Shipping</span>
          <span v-if="shippingCents === 0" class="text-positive">{{ freeShippingLabel }}</span>
          <Price v-else :price="{ amount: shippingCents, currency }" />
        </div>
        <div v-if="showTax" class="flex justify-between">
          <span class="text-muted-foreground">Tax</span>
          <Price :price="{ amount: taxCents, currency }" />
        </div>
      </div>

      <Separator />

      <div class="flex justify-between text-base font-semibold">
        <span>Total</span>
        <Price :price="{ amount: totalCents, currency }" />
      </div>
    </CardContent>
  </Card>
</template>
Order summary card with product thumbnails, quantity badges, and subtotal/shipping/tax/total breakdown.
order-summary