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.
Preview
-
Starter
For one person getting started.
$9 per month- Seats
- 1
- Storage
- 10 GB
- Community support
- Basic analytics
-
Team
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
<x-ui.comparison-cards class="mx-auto w-full max-w-3xl" />
Installation
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.
-
resources/views/components/ui/comparison-cards.blade.php
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.
# 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
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| options | array | [] | |
| metricLabel | string | Price | |
| hero | bool | true |
Slots
default—
Data slots
Stable hooks for CSS overrides and browser tests.
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
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
- Theming hooks
Accessibility
- 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-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, 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 underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
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.
@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