
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Plan comparison feature matrix (Starter / Pro / Enterprise) with a sticky header row, monthly/yearly ToggleGroup that flips prices live, grouped feature rows (Core / Collaboration / Support), and check / minus / short-value cells. The Pro column is highlighted with a "Popular" badge and a bg-primary/5 tint down the whole column.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/comparison-table.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/comparison-table.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/comparison-table.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/comparison-table.jsonnpx shadcn-vue@latest add @uipkge/comparison-tableInstalls to:app/components/blocks/Type aliases exported from this item's source. Use these to shape the data you pass in.
Planinterface Plan {
name: string
monthly: number
yearly: number
cta: string
popular?: boolean
}Featureinterface Feature {
label: string
cells: [Cell, Cell, Cell]
}Groupinterface Group {
label: string
features: Feature[]
}<script setup lang="ts">
import { ref } from 'vue'
import { Check, Minus } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
type Cycle = 'monthly' | 'yearly'
type Cell = boolean | string | number
interface Plan {
name: string
monthly: number
yearly: number
cta: string
popular?: boolean
}
interface Feature {
label: string
cells: [Cell, Cell, Cell]
}
interface Group {
label: string
features: Feature[]
}
const cycle = ref<Cycle>('monthly')
const plans: Plan[] = [
{ name: 'Starter', monthly: 9, yearly: 7, cta: 'Start free' },
{ name: 'Pro', monthly: 29, yearly: 24, cta: 'Start 14-day trial', popular: true },
{ name: 'Enterprise', monthly: 79, yearly: 65, cta: 'Talk to sales' },
]
const groups: Group[] = [
{
label: 'Core',
features: [
{ label: 'Projects', cells: [3, 'Unlimited', 'Unlimited'] },
{ label: 'Team seats', cells: ['5 seats', '20 seats', 'Custom'] },
{ label: 'API access', cells: [false, true, true] },
],
},
{
label: 'Collaboration',
features: [
{ label: 'Shared workspaces', cells: [false, true, true] },
{ label: 'Guest reviewers', cells: [false, '5 guests', 'Unlimited'] },
{ label: 'Roles & permissions', cells: [true, true, true] },
{ label: 'Audit log', cells: [false, false, true] },
],
},
{
label: 'Support',
features: [
{ label: 'Support channel', cells: ['Email', 'Priority email', 'Dedicated CSM'] },
{ label: 'Guided onboarding', cells: [false, true, true] },
{ label: 'Uptime SLA', cells: [false, false, '99.9%'] },
],
},
]
</script>
<template>
<section data-slot="comparison-table" class="bg-background w-full">
<div class="mx-auto max-w-5xl px-6 py-16">
<div class="mb-8 flex flex-col items-center gap-4 text-center">
<h2 class="text-2xl font-semibold tracking-tight sm:text-3xl">Compare plans</h2>
<ToggleGroup type="single" :model-value="cycle" @update:model-value="(v) => v && (cycle = v as Cycle)">
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
<ToggleGroupItem value="yearly">
Yearly
<Badge variant="secondary" class="ml-2">−20%</Badge>
</ToggleGroupItem>
</ToggleGroup>
</div>
<div class="overflow-x-auto rounded-lg border">
<table class="w-full max-w-[640px] min-w-full border-separate border-spacing-0 text-sm">
<caption class="sr-only">
Feature comparison across Starter, Pro, and Enterprise plans
</caption>
<thead>
<tr>
<th
scope="col"
class="bg-background/95 supports-[backdrop-filter]:bg-background/80 text-muted-foreground sticky top-0 left-0 z-10 w-1/4 border-b p-4 text-left font-medium backdrop-blur"
>
<span class="sr-only">Feature</span>
</th>
<th
v-for="plan in plans"
:key="plan.name"
scope="col"
class="sticky top-0 z-10 border-b p-4 text-center align-bottom"
:class="
plan.popular
? 'bg-primary/10 supports-[backdrop-filter]:bg-primary/10 backdrop-blur'
: 'bg-background/95 supports-[backdrop-filter]:bg-background/80 backdrop-blur'
"
>
<div class="flex flex-col items-center gap-1.5">
<Badge v-if="plan.popular">Popular</Badge>
<span class="text-base font-semibold tracking-tight">{{ plan.name }}</span>
<span class="tabular-nums">
<span class="text-2xl font-semibold">${{ cycle === 'monthly' ? plan.monthly : plan.yearly }}</span>
<span class="text-muted-foreground text-xs"> /mo</span>
</span>
<Button size="sm" :variant="plan.popular ? 'default' : 'outline'" class="mt-1 w-full">
{{ plan.cta }}
</Button>
</div>
</th>
</tr>
</thead>
<tbody v-for="group in groups" :key="group.label">
<tr>
<th
colspan="4"
scope="colgroup"
class="bg-muted/50 text-muted-foreground border-b p-3 text-left text-xs font-semibold tracking-widest uppercase"
>
{{ group.label }}
</th>
</tr>
<tr v-for="feature in group.features" :key="feature.label" class="hover:bg-muted/30">
<th scope="row" class="border-b p-4 text-left align-middle font-normal">
{{ feature.label }}
</th>
<td
v-for="(cell, i) in feature.cells"
:key="i"
class="border-b p-4 text-center align-middle"
:class="{ 'bg-primary/5': plans[i].popular }"
>
<Check v-if="cell === true" class="text-foreground mx-auto size-4" aria-label="Included" />
<Minus
v-else-if="cell === false"
class="text-muted-foreground/50 mx-auto size-4"
aria-label="Not included"
/>
<span v-else class="text-xs font-medium">{{ cell }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</template>
Raw manifest:https://uipkge.dev/r/vue/comparison-table.json