Skip to main content

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>