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
<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
<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