Data display
Pricing Table code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/pricing-table.blade.php
@props([
'plans' => [],
'monthlyLabel' => 'Monthly',
'annualLabel' => 'Annual',
'annualNote' => 'Save 20%',
'defaultAnnual' => false,
])
@php
/*
| Pricing table with a monthly/annual billing toggle. Prices switch
| reactively without a page reload. Pass plans as:
| ['name' => 'Pro', 'description' => '...', 'monthly' => '$29', 'annual' => '$23',
| 'period' => '/mo', 'features' => ['...'], 'featured' => true,
| 'badge' => 'Most popular', 'cta' => 'Get started', 'href' => '#']
|
| Usage:
| <x-ui.pricing-table :plans="$plans" annual-note="Save 20%" />
*/
$planData = collect($plans)->map(fn (array $p): array => [
'name' => $p['name'] ?? '',
'description' => $p['description'] ?? null,
'monthly' => $p['monthly'] ?? '',
'annual' => $p['annual'] ?? ($p['monthly'] ?? ''),
'period' => $p['period'] ?? '/mo',
'features' => array_values($p['features'] ?? []),
'featured' => (bool) ($p['featured'] ?? false),
'badge' => $p['badge'] ?? 'Most popular',
'cta' => $p['cta'] ?? 'Get started',
'href' => $p['href'] ?? '#',
])->values()->all();
@endphp
<div
x-data="{
annual: @js((bool) $defaultAnnual),
focusIndex: @js((bool) $defaultAnnual ? 1 : 0),
focusOption(index) {
this.$nextTick(() => this.$refs[index === 0 ? 'monthly' : 'annual']?.focus());
},
move(direction) {
const next = this.focusIndex === 0 ? 1 : 0;
if (direction === 0) {
this.focusOption(this.focusIndex);
return;
}
this.focusIndex = next;
this.annual = next === 1;
this.focusOption(next);
},
choose(index) {
this.focusIndex = index;
this.annual = index === 1;
},
}"
{{ $attributes->merge(['class' => 'w-full']) }}
>
<div class="flex items-center justify-center gap-4">
<div class="inline-flex items-center gap-1 rounded-xl bg-gray-100 p-1 dark:bg-gray-800" role="radiogroup" aria-label="Billing period">
<button
x-ref="monthly"
type="button"
role="radio"
:aria-checked="(! annual).toString()"
:tabindex="! annual ? '0' : '-1'"
@click="choose(0)"
@keydown.arrow-right.prevent="move(1)"
@keydown.arrow-down.prevent="move(1)"
@keydown.arrow-left.prevent="move(-1)"
@keydown.arrow-up.prevent="move(-1)"
@keydown.home.prevent="choose(0); focusOption(0)"
@keydown.end.prevent="choose(1); focusOption(1)"
:class="! annual ? 'bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
class="rounded-lg px-3.5 py-1.5 text-sm font-medium transition"
>{{ $monthlyLabel }}</button>
<button
x-ref="annual"
type="button"
role="radio"
:aria-checked="annual.toString()"
:tabindex="annual ? '0' : '-1'"
@click="choose(1)"
@keydown.arrow-right.prevent="move(1)"
@keydown.arrow-down.prevent="move(1)"
@keydown.arrow-left.prevent="move(-1)"
@keydown.arrow-up.prevent="move(-1)"
@keydown.home.prevent="choose(0); focusOption(0)"
@keydown.end.prevent="choose(1); focusOption(1)"
:class="annual ? 'bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'"
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-1.5 text-sm font-medium transition"
>
{{ $annualLabel }}
@if ($annualNote)
<span class="rounded-full bg-brand-100 px-2 py-0.5 text-[0.6875rem] font-semibold text-brand-700 dark:bg-brand-950/60 dark:text-brand-300">{{ $annualNote }}</span>
@endif
</button>
</div>
</div>
<div class="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($planData as $plan)
@php
$featured = $plan['featured'];
$wrap = $featured
? 'relative border-2 border-brand-500 bg-brand-50 shadow-xl shadow-brand-500/10 lg:scale-[1.03] dark:bg-brand-950/40'
: 'border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900';
$planColor = 'text-gray-900 dark:text-white';
$periodColor = 'text-gray-500 dark:text-gray-400';
$descColor = 'text-gray-600 dark:text-gray-400';
$featColor = 'text-gray-600 dark:text-gray-300';
@endphp
<div class="flex flex-col rounded-2xl p-8 {{ $wrap }}">
@if ($featured)
<span class="absolute -top-3 left-1/2 -translate-x-1/2">
<x-ui.badge variant="brand" class="bg-brand-500 text-white shadow dark:bg-brand-500 dark:text-white">{{ $plan['badge'] }}</x-ui.badge>
</span>
@endif
<h3 class="text-lg font-semibold {{ $planColor }}">{{ $plan['name'] }}</h3>
@if ($plan['description'])
<p class="mt-1 text-sm {{ $descColor }}">{{ $plan['description'] }}</p>
@endif
<div class="mt-5 flex items-baseline gap-1">
<span class="text-4xl font-semibold tracking-tight {{ $planColor }}" x-text="annual ? @js($plan['annual']) : @js($plan['monthly'])"></span>
<span class="text-sm {{ $periodColor }}">{{ $plan['period'] }}</span>
</div>
<p class="mt-1 h-4 text-xs {{ $periodColor }}" x-show="annual" x-cloak>{{ $annualLabel }} billing</p>
<div class="mt-6">
<x-ui.button :href="$plan['href']" :variant="$featured ? 'accent' : 'outline'" block>{{ $plan['cta'] }}</x-ui.button>
</div>
@if (count($plan['features']))
<ul class="mt-8 space-y-3.5 text-sm">
@foreach ($plan['features'] as $feature)
<li class="flex items-start gap-3 {{ $featColor }}">
<x-ui.icon name="check-circle" weight="fill" size="lg" class="shrink-0 text-brand-500" />
<span>{{ $feature }}</span>
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
</div>
</div>