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 { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import { Rating } from '@/components/ui/rating'
interface ReviewItem {
id: string
author: string
avatar?: string
rating: number
content: string
}
interface RatingCount {
rating: number
count: number
}
const props = withDefaults(
defineProps<{
average?: number
totalCount?: number
counts?: RatingCount[]
reviews?: ReviewItem[]
}>(),
{
average: 4.3,
totalCount: 1624,
counts: () => [
{ rating: 5, count: 1019 },
{ rating: 4, count: 162 },
{ rating: 3, count: 97 },
{ rating: 2, count: 199 },
{ rating: 1, count: 147 },
],
reviews: () => [
{
id: '1',
rating: 5,
author: 'Emily Selman',
content: 'This jacket is incredible. Kept me warm and dry through a full week of backcountry skiing in the Dolomites. The vents are a game-changer for uphill sections — I never overheated once.',
},
{
id: '2',
rating: 5,
author: 'Hector Gibbons',
content: 'Best ski jacket I\'ve ever owned. The build quality is outstanding — taped seams, solid zippers, and the articulated fit lets me move freely on every run. Worth every penny for serious riders.',
},
{
id: '3',
rating: 4,
author: 'Mark Edwards',
content: 'Really happy with the warmth and breathability. Used it for a full season in the Alps and it performed perfectly in everything from bluebird days to heavy snow. Only wish the goggle pocket was a bit larger.',
},
],
},
)
function getInitials(name: string): string {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
}
function percentage(count: number): number {
return props.totalCount > 0 ? Math.round((count / props.totalCount) * 100) : 0
}
</script>
<template>
<div class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:grid lg:max-w-7xl lg:grid-cols-12 lg:gap-x-8 lg:px-8 lg:py-32">
<!-- Left: Summary -->
<div class="lg:col-span-4">
<h2 class="text-2xl font-bold tracking-tight">Customer Reviews</h2>
<!-- Average rating -->
<div class="mt-3 flex items-center">
<Rating :rating="average" :max="5" />
<p class="ml-2 text-sm">Based on {{ totalCount }} reviews</p>
</div>
<!-- Rating distribution bars -->
<div class="mt-6">
<h3 class="sr-only">Review data</h3>
<dl class="space-y-3">
<div v-for="count in counts" :key="count.rating" class="flex items-center text-sm">
<dt class="flex flex-1 items-center">
<p class="w-3 font-medium">{{ count.rating }}<span class="sr-only"> star reviews</span></p>
<Rating :rating="count.count > 0 ? 1 : 0" :max="1" class="ml-1 shrink-0" />
<Progress :model-value="percentage(count.count)" class="ml-3" />
</dt>
<dd class="ml-3 w-10 text-right text-sm tabular-nums">{{ percentage(count.count) }}%</dd>
</div>
</dl>
</div>
<!-- Write a review CTA -->
<div class="mt-10">
<h3 class="text-lg font-medium">Share your thoughts</h3>
<p class="mt-1 text-sm text-muted-foreground">If you've used this product, share your thoughts with other customers</p>
<Button variant="outline" class="mt-6 w-full lg:w-full sm:w-auto">
Write a review
</Button>
</div>
</div>
<!-- Right: Featured reviews -->
<div class="mt-16 lg:col-span-7 lg:col-start-6 lg:mt-0">
<h3 class="sr-only">Recent reviews</h3>
<div class="-my-12 divide-y">
<div v-for="review in reviews" :key="review.id" class="py-12">
<div class="flex items-center">
<Avatar class="size-12">
<AvatarImage v-if="review.avatar" :src="review.avatar" :alt="review.author" />
<AvatarFallback>{{ getInitials(review.author) }}</AvatarFallback>
</Avatar>
<div class="ml-4">
<h4 class="text-sm font-bold">{{ review.author }}</h4>
<Rating :rating="review.rating" :max="5" class="mt-1" />
</div>
</div>
<p class="mt-4 text-sm/6 text-muted-foreground italic">{{ review.content }}</p>
</div>
</div>
</div>
</div>
</template>
Review section with rating summary, distribution bars, and individual reviews.
review-cards
Files
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Rating } from '@/components/ui/rating'
withDefaults(
defineProps<{
title?: string
reviews?: ReviewItem[]
}>(),
{
title: 'Customer Reviews',
reviews: () => mockReviews,
},
)
function getInitials(name: string): string {
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
}
function formatDate(dateStr: string): string {
try {
return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
catch {
return dateStr
}
}
</script>
<script lang="ts">
interface ReviewItem {
id: string
author: string
avatar?: string
date: string
rating: number
content: string
}
const mockReviews: ReviewItem[] = [
{
id: '1',
author: 'Emily Selman',
date: '2025-01-16',
rating: 5,
content: 'Absolutely love this jacket. Kept me warm and dry during a full day of backcountry riding in heavy snow. The hood fits perfectly over my helmet and the vents work great for uphill sections.',
},
{
id: '2',
author: 'Hector Gibbons',
date: '2025-01-12',
rating: 5,
content: 'Best ski jacket I\'ve owned. The build quality is outstanding — taped seams, solid zippers, and the fit allows full range of motion. Worth every penny for serious riders.',
},
{
id: '3',
author: 'Mark Edwards',
date: '2025-01-06',
rating: 4,
content: 'Really happy with the warmth and breathability. Used it for a week in the Alps and it performed perfectly. Only wish the pockets were a bit larger for goggles.',
},
]
</script>
<template>
<div class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8">
<h2 class="sr-only">{{ title }}</h2>
<div class="-my-10">
<div
v-for="(review, index) in reviews"
:key="review.id"
class="flex gap-4 text-sm"
>
<!-- Avatar -->
<div class="shrink-0 py-10">
<Avatar class="size-10">
<AvatarImage v-if="review.avatar" :src="review.avatar" :alt="review.author" />
<AvatarFallback>{{ getInitials(review.author) }}</AvatarFallback>
</Avatar>
</div>
<!-- Content -->
<div
class="flex-1 py-10"
:class="index > 0 && 'border-t'"
>
<h3 class="font-medium">{{ review.author }}</h3>
<p class="text-muted-foreground">
<time :datetime="review.date">{{ formatDate(review.date) }}</time>
</p>
<Rating :rating="review.rating" :max="5" class="mt-4" />
<p class="mt-4 text-sm/6 text-muted-foreground">{{ review.content }}</p>
</div>
</div>
</div>
</div>
</template>
Simple review list with avatars, star ratings, author, date, and review text.
review-list