Skip to content
Brok UI

Loading…

No results

Comparison Cards

Open source

A responsive grid of option cards: name, description, a hero metric with a note, attribute rows, a feature check list and a call to action, with one option highlighted as recommended.

Version
v2.1.2
Stability
stable
License
MIT
Related
Card
Goal Card

Preview

  • Starter

    For one person getting started.

    $9 per month
    Seats
    1
    Storage
    10 GB
    • Community support
    • Basic analytics
  • Team Recommended

    For small teams shipping together.

    $29 per month
    Seats
    5
    Storage
    100 GB
    • Priority support
    • Advanced analytics
    • Shared workspaces
  • Scale

    For companies with many teams.

    $99 per month
    Seats
    Unlimited
    Storage
    1 TB
    • Dedicated support
    • SSO and audit log
    • Custom contracts
previews.components.comparison-cards.default.blade.php Blade
<x-ui.comparison-cards class="mx-auto w-full max-w-3xl" />

Installation

terminal
php artisan ui:add comparison-cards

Registry contract

php artisan ui:add comparison-cards writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/comparison-cards.blade.php
Registry dependencies
badge button
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

comparison-cards.md
# Brok UI: Comparison Cards (`comparison-cards`)

A responsive grid of option cards: name, description, a hero metric with a note, attribute rows, a feature check list and a call to action, with one option highlighted as recommended.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add comparison-cards
```

## Usage

```blade
<x-ui.comparison-cards class="mx-auto w-full max-w-3xl" />
```

## Props

- `options` (array, default `[]`)
- `metricLabel` (string, default `Price`)
- `hero` (bool, default `true`)

## Use when

- Use when self-contained content objects benefit from a recognizable card-like presentation with local actions or metadata.
- Comparing a few options side by side, each with a price or hero metric, attributes and a feature list.
- Presenting plans, tiers or packages where one should read as the recommended choice.

## Avoid when

- Do not use card widgets where a simpler list, table, or plain text block would make comparison faster.
- Comparing many attributes across many products; a comparison table reads better than a card per option.
- Listing options that carry no comparable metric; card grids without the hero row are simpler.

## Anti-patterns

- Using cards for dense cross-item comparison

## Rules

- Use the `<brok:comparison-cards>` tag (or `<x-ui.comparison-cards>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/comparison-cards
- Registry JSON (files, props, contract): https://brokui.dev/r/open/comparison-cards.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
options array []
metricLabel string Price
hero bool true

Slots

  • default —

Data slots

Stable hooks for CSS overrides and browser tests.

comparison-cards comparison-cards-card comparison-cards-metric

Behavior

  • options renders one card per option in a responsive grid; the option flagged as recommended is highlighted.
  • metricLabel names the hero metric on every card, and hero turns that row off entirely.
  • Attribute rows, the feature check list and the call to action appear only when the option supplies them.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized, reducedMotion.

Guidance

Card and content widget

Present a self-contained content object with local context.

Use when

  • Use when self-contained content objects benefit from a recognizable card-like presentation with local actions or metadata.
  • Comparing a few options side by side, each with a price or hero metric, attributes and a feature list.
  • Presenting plans, tiers or packages where one should read as the recommended choice.

Avoid when

  • Do not use card widgets where a simpler list, table, or plain text block would make comparison faster.
  • Comparing many attributes across many products; a comparison table reads better than a card per option.
  • Listing options that carry no comparable metric; card grids without the hero row are simpler.

Use instead

  • List for compact scanning
  • Table for exact comparison

Anti-patterns

  • Using cards for dense cross-item comparison
Anatomy
root comparison-cards-card comparison-cards-metric
Theming hooks
comparison-cards comparison-cards-card comparison-cards-metric

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
No component-owned keyboard interaction; native element behavior applies.
Focus
none
  • The grid is a list of cards, and the recommended card carries aria-current so it is announced as such.
  • Feature checks are text with an aria-hidden icon; recommendation is never conveyed by colour alone.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Safe

Livewire can update this component through forwarded wire:* attributes.

Source

The exact, editable file ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/comparison-cards.blade.php Blade
@props([
    'options' => [],
    // Label for the metric row when it renders as a plain attribute (no `hero`).
    'metricLabel' => 'Price',
    // Show the metric as the card's hero figure (large, with `metricNote`
    // under it) instead of as the last attribute row.
    'hero' => true,
])

@php
    $options = is_array($options) && count($options)
        ? $options
        : [
            ['name' => 'Starter', 'description' => __('For one person getting started.'), 'metric' => '$9', 'metricNote' => __('per month'), 'attributes' => [['label' => __('Seats'), 'value' => '1'], ['label' => __('Storage'), 'value' => '10 GB']], 'features' => [__('Community support'), __('Basic analytics')], 'cta' => ['label' => __('Choose Starter'), 'href' => '#']],
            ['name' => 'Team', 'description' => __('For small teams shipping together.'), 'metric' => '$29', 'metricNote' => __('per month'), 'attributes' => [['label' => __('Seats'), 'value' => '5'], ['label' => __('Storage'), 'value' => '100 GB']], 'features' => [__('Priority support'), __('Advanced analytics'), __('Shared workspaces')], 'badge' => __('Recommended'), 'recommended' => true, 'cta' => ['label' => __('Choose Team'), 'href' => '#']],
            ['name' => 'Scale', 'description' => __('For companies with many teams.'), 'metric' => '$99', 'metricNote' => __('per month'), 'attributes' => [['label' => __('Seats'), 'value' => __('Unlimited')], ['label' => __('Storage'), 'value' => '1 TB']], 'features' => [__('Dedicated support'), __('SSO and audit log'), __('Custom contracts')], 'cta' => ['label' => __('Contact sales'), 'href' => '#']],
        ];
    $hasRecommended = collect($options)->contains(fn ($o) => ! empty($o['recommended'] ?? false));
@endphp

{{--
    Comparison cards — side-by-side option cards in a responsive grid. Each
    card carries a name, an optional description, a hero metric (a price, a
    limit) with a note under it, label/value attribute rows, a check list of
    features and a call to action. Any one card may be flagged `recommended`;
    that flag drives the primary border and `aria-current="true"`. The
    optional `badge` pill is independent. If no card is flagged, the first
    one is treated as recommended. Metrics and values use tabular figures so
    the columns align across cards.
--}}
<ul
    data-slot="comparison-cards"
    role="list"
    {{ $attributes->merge(['class' => 'grid list-none gap-4 p-0 sm:grid-cols-2 md:grid-cols-3']) }}
>
    @foreach ($options as $i => $option)
        @php
            $name = $option['name'] ?? '';
            $description = $option['description'] ?? null;
            $attributes_ = is_array($option['attributes'] ?? null) ? $option['attributes'] : [];
            $features = is_array($option['features'] ?? null) ? $option['features'] : [];
            $metric = $option['metric'] ?? null;
            $metricNote = $option['metricNote'] ?? null;
            $badge = $option['badge'] ?? null;
            $cta = is_array($option['cta'] ?? null) ? $option['cta'] : null;
            $recommended = $hasRecommended ? (bool) ($option['recommended'] ?? false) : ($i === 0);
        @endphp
        <li
            data-slot="comparison-cards-card"
            @if ($recommended) aria-current="true" @endif
            @class([
                'flex flex-col gap-4 rounded-lg border bg-card p-6 text-foreground',
                'border-primary shadow-sm' => $recommended,
                'border-border' => ! $recommended,
            ])
        >
            <div class="flex flex-col gap-1">
                <div class="flex items-start justify-between gap-2">
                    <span class="font-semibold">{{ $name }}</span>
                    @if ($badge)
                        <x-ui.badge size="sm" class="shrink-0">{{ $badge }}</x-ui.badge>
                    @endif
                </div>
                @if ($description)
                    <p class="text-sm text-muted-foreground">{{ $description }}</p>
                @endif
            </div>

            @if ($hero && $metric !== null)
                <div class="flex items-baseline gap-2">
                    <span data-slot="comparison-cards-metric" class="text-2xl font-semibold tabular-nums tracking-tight">{{ $metric }}</span>
                    @if ($metricNote)<span class="text-sm text-muted-foreground">{{ $metricNote }}</span>@endif
                </div>
            @endif

            @if ($attributes_ !== [] || (! $hero && $metric !== null))
                <dl class="divide-y divide-border text-sm">
                    @foreach ($attributes_ as $row)
                        @php
                            $rowLabel = $row['label'] ?? null;
                            $rowValue = $row['value'] ?? null;
                        @endphp
                        @if ($rowLabel !== null || $rowValue !== null)
                            <div class="flex items-center justify-between gap-2 py-2">
                                <dt class="text-muted-foreground">{{ $rowLabel }}</dt>
                                <dd class="font-medium tabular-nums">{{ $rowValue }}</dd>
                            </div>
                        @endif
                    @endforeach
                    @if (! $hero && $metric !== null)
                        <div class="flex items-center justify-between gap-2 py-2">
                            <dt class="text-muted-foreground">{{ __($metricLabel) }}</dt>
                            <dd class="font-medium tabular-nums">{{ $metric }}</dd>
                        </div>
                    @endif
                </dl>
            @endif

            @if ($features !== [])
                <ul class="space-y-2 text-sm" role="list">
                    @foreach ($features as $feature)
                        <li class="flex gap-2">
                            <svg class="mt-px size-4 shrink-0 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 5 5L20 7" /></svg>
                            <span class="min-w-0">{{ $feature }}</span>
                        </li>
                    @endforeach
                </ul>
            @endif

            @if ($cta && filled($cta['label'] ?? null))
                <div class="mt-auto pt-2">
                    <x-ui.button :href="$cta['href'] ?? null" :variant="$recommended ? 'default' : 'outline'" class="w-full">{{ $cta['label'] }}</x-ui.button>
                </div>
            @endif
        </li>
    @endforeach
</ul>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
2.1.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0