Quote Card
The testimonial figure: an optional quote glyph or star rating, the blockquote, then the attribution row (avatar, name, role and company), framed in the card surface or bare for a surface that already holds other media.
Preview
We replaced three half-finished design systems with this in a weekend. The token model means re-theming is a config change, not a refactor.
Avery Mitchell
Head of Design, Northwind
<x-ui.quote-card
class="max-w-md"
:quote="__('We replaced three half-finished design systems with this in a weekend. The token model means re-theming is a config change, not a refactor.')"
:name="__('Avery Mitchell')"
:role="__('Head of Design')"
:company="__('Northwind')"
/>
Variant options
Size options
Installation
php artisan ui:add quote-card
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: Quote Card (`quote-card`)
The testimonial figure: an optional quote glyph or star rating, the blockquote, then the attribution row (avatar, name, role and company), framed in the card surface or bare for a surface that already holds other media.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add quote-card
```
## Usage
```blade
<x-ui.quote-card
class="max-w-md"
:quote="__('We replaced three half-finished design systems with this in a weekend. The token model means re-theming is a config change, not a refactor.')"
:name="__('Avery Mitchell')"
:role="__('Head of Design')"
:company="__('Northwind')"
/>
```
## Props
- `quote` (string|null, default `null`) — The quoted text. Omit it and use the default slot for rich content.
- `name` (string|null, default `null`) — Who said it. Without a name the caption row is not drawn.
- `role` (string|null, default `null`) — Job title, joined to `company` with a comma.
- `company` (string|null, default `null`) — Organisation, joined after `role`.
- `avatar` (string|null, default `null`) — Image URL for the avatar; falls back to `initials`.
- `initials` (string|null, default `null`) — Avatar fallback. Derived from the first two words of `name` when omitted.
- `rating` (float|null, default `null`) — Draws a static `rating display` star row above the quote in place of the glyph.
- `glyph` ('serif'|'solid'|'none', default `serif`) — The quotation mark that leads the figure: serif in the muted tone, solid in the primary tone, or none (the quote is then wrapped in typographic quotes).
- `quotes` (bool|null, default `null`) — Wrap the text in typographic quotation marks. Defaults to true only when `glyph` is `none`.
- `variant` ('card'|'plain', default `card`) — `card` frames the figure in the card surface with 24px inset; `plain` is the bare figure (a ghost card) for a surface that already holds media.
- `size` ('sm'|'md'|'lg', default `sm`) — Quote type size: text-sm, text-base, or text-lg/xl for a hero carousel.
- `avatarSize` ('sm'|'md'|'lg', default `md`) — Size passed to the avatar in the caption.
- `divider` (bool, default `true`) — Draws a separator between the quote and the caption.
## Use when
- Use when self-contained content objects benefit from a recognizable card-like presentation with local actions or metadata.
- A testimonials grid, marquee, carousel or split needs one customer quote with who said it.
- A review needs a star score above the quote — pass `rating`.
## Avoid when
- Do not use card widgets where a simpler list, table, or plain text block would make comparison faster.
- The quote is a social post with handle, timestamp and engagement counts — use `twitter-card`.
- The quote is a pull quote inside long-form copy — use `prose`'s blockquote.
## Anti-patterns
- Using cards for dense cross-item comparison
## Rules
- Use the `<brok:quote-card>` tag (or `<x-ui.quote-card>`) 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/quote-card
- Registry JSON (files, props, contract): https://brokui.dev/r/open/quote-card.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
<div class="grid max-w-4xl gap-6 lg:grid-cols-3">
<x-ui.quote-card glyph="serif" :quote="__('Owning the source is the whole game. No runtime lock-in, no upgrade anxiety.')" :name="__('Jordan Lee')" :role="__('Staff Engineer')" :company="__('Lumen')" />
<x-ui.quote-card glyph="solid" :quote="__('Owning the source is the whole game. No runtime lock-in, no upgrade anxiety.')" :name="__('Jordan Lee')" :role="__('Staff Engineer')" :company="__('Lumen')" />
<x-ui.quote-card glyph="none" :divider="false" :quote="__('Owning the source is the whole game. No runtime lock-in, no upgrade anxiety.')" :name="__('Jordan Lee')" :role="__('Staff Engineer')" :company="__('Lumen')" />
</div>
Long Content
<div class="max-w-xs">
<x-ui.quote-card
:quote="__('A deliberately long testimonial that keeps going well past the width of the card to verify the blockquote wraps, the caption stays on one line with truncation, and nothing overflows the surface: https://example.com/a-very-long-url-with-no-break-points-at-all')"
:name="__('Maximiliana Konstantinopoulou-Featherstonehaugh')"
:role="__('Vice President of Engineering and Platform Reliability')"
:company="__('International Business Machines Corporation')"
/>
</div>
<x-ui.card class="max-w-md overflow-hidden p-0">
<div class="aspect-video w-full bg-muted" role="img" aria-label="{{ __('Sarah Chen speaking on camera') }}"></div>
<x-ui.quote-card
variant="plain"
glyph="none"
class="p-6"
avatar-size="sm"
:quote="__('Onboarding our team went from a week-long slog to a single afternoon.')"
:name="__('Sarah Chen')"
:role="__('Product Lead')"
:company="__('Luminary Labs')"
/>
</x-ui.card>
<x-ui.quote-card
class="max-w-md"
:rating="5"
:quote="__('Accessibility used to be a backlog item. Now keyboard support and focus states ship by default with every block.')"
:name="__('Priya Nair')"
:role="__('Product Lead')"
:company="__('Cadence')"
/>
<div class="flex max-w-3xl flex-col gap-6">
<x-ui.quote-card size="sm" :quote="__('Setup took minutes. We were composing on-brand pages before lunch.')" :name="__('Daniel Okafor')" :role="__('Engineering Manager')" :company="__('Beacon')" />
<x-ui.quote-card size="md" :quote="__('Setup took minutes. We were composing on-brand pages before lunch.')" :name="__('Daniel Okafor')" :role="__('Engineering Manager')" :company="__('Beacon')" />
<x-ui.quote-card size="lg" glyph="solid" avatar-size="lg" :quote="__('Setup took minutes. We were composing on-brand pages before lunch.')" :name="__('Daniel Okafor')" :role="__('Engineering Manager')" :company="__('Beacon')" />
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| quote | string | null | null | The quoted text. Omit it and use the default slot for rich content. |
| name | string | null | null | Who said it. Without a name the caption row is not drawn. |
| role | string | null | null | Job title, joined to `company` with a comma. |
| company | string | null | null | Organisation, joined after `role`. |
| avatar | string | null | null | Image URL for the avatar; falls back to `initials`. |
| initials | string | null | null | Avatar fallback. Derived from the first two words of `name` when omitted. |
| rating | float | null | null | Draws a static `rating display` star row above the quote in place of the glyph. |
| glyph | 'serif' | 'solid' | 'none' | serif | The quotation mark that leads the figure: serif in the muted tone, solid in the primary tone, or none (the quote is then wrapped in typographic quotes). |
| quotes | bool | null | null | Wrap the text in typographic quotation marks. Defaults to true only when `glyph` is `none`. |
| variant | 'card' | 'plain' | card | `card` frames the figure in the card surface with 24px inset; `plain` is the bare figure (a ghost card) for a surface that already holds media. |
| size | 'sm' | 'md' | 'lg' | sm | Quote type size: text-sm, text-base, or text-lg/xl for a hero carousel. |
| avatarSize | 'sm' | 'md' | 'lg' | md | Size passed to the avatar in the caption. |
| divider | bool | true | Draws a separator between the quote and the caption. |
Slots
default— Rich quote content when `quote` is not set.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- The root is `<x-ui.card as="figure">`, so the surface, radius and hover recipe stay the card's; `plain` maps to the card's ghost variant.
- `role` and `company` are joined with one comma in one line, so every testimonials block reads the same.
- The blockquote is `flex-1`, so cards in a grid row keep their captions on one baseline.
- Declares registry capability flags: a11y, authoredStateFixtures, responsive, rtl, darkMode, localized.
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.
- A testimonials grid, marquee, carousel or split needs one customer quote with who said it.
- A review needs a star score above the quote — pass `rating`.
Avoid when
- Do not use card widgets where a simpler list, table, or plain text block would make comparison faster.
- The quote is a social post with handle, timestamp and engagement counts — use `twitter-card`.
- The quote is a pull quote inside long-form copy — use `prose`'s blockquote.
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
- Rendered as a `<figure>` with a `<blockquote>` and a `<figcaption>`, so assistive technology reads the quote and its attribution as one unit.
- The quote glyph is decorative; a rating is a `role="img"` with the score in its label.
- 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.
{{--
Quote Card — the testimonial figure every testimonials block used to draw
by hand: an optional quote glyph or star rating, the blockquote, then the
attribution row (avatar, name, role and company). One anatomy, one
role/company joiner, one set of glyphs.
`quote` is the text (or use the default slot for rich content). `glyph`
leads with a serif quotation mark in the muted tone, a solid mark in the
primary tone, or nothing; when there is no glyph the quote is wrapped in
typographic quotation marks unless `quotes` says otherwise. `rating`
draws a static star row above the quote. `variant="card"` frames the
figure in the card surface; `plain` is the bare figure (a ghost card) for
a surface that already holds other media. `size` sets the quote type
only; `divider` draws a hairline between the quote and the caption.
--}}
@props([
'quote' => null,
'name' => null,
'role' => null,
'company' => null,
'avatar' => null,
'initials' => null,
'rating' => null,
'glyph' => 'serif',
'quotes' => null,
'variant' => 'card',
'size' => 'sm',
'avatarSize' => 'md',
'divider' => true,
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$recipe = $styles['quote-card'] ?? [];
$variant = $styles['normalizeVariant']($variant);
$variant = in_array($variant, ['card', 'plain'], true) ? $variant : 'card';
$glyph = in_array($glyph, ['serif', 'solid', 'none'], true) ? $glyph : 'serif';
$size = array_key_exists($size, $recipe['sizes'] ?? []) ? $size : 'sm';
$divider = filter_var($divider, FILTER_VALIDATE_BOOLEAN);
$quotes = $quotes === null ? $glyph === 'none' : filter_var($quotes, FILTER_VALIDATE_BOOLEAN);
$hasRating = $rating !== null && (float) $rating > 0;
// Initials fall back to the first letter of each of the first two words.
$fallback = $initials ?? (function (?string $name): string {
$parts = preg_split('/\s+/', trim((string) $name)) ?: [];
return mb_strtoupper(collect($parts)->filter()->take(2)->map(fn ($p) => mb_substr($p, 0, 1))->implode(''));
})($name);
$meta = implode(', ', array_filter([$role, $company], fn ($v) => $v !== null && $v !== ''));
$figureClass = trim(($recipe['figure'] ?? '').($variant === 'card' ? ' p-6' : ''));
$quoteClass = trim(($recipe['quote'] ?? '').' '.($recipe['sizes'][$size] ?? ''));
$glyphPaths = [
'serif' => '<path d="M9.6 4.8C6.2 6.3 4 9.5 4 13.2V19.2h6v-6H7.2c0-2.5 1.4-4.6 3.6-5.7L9.6 4.8Zm9 0c-3.4 1.5-5.6 4.7-5.6 8.4V19.2h6v-6H16c0-2.5 1.4-4.6 3.6-5.7L18.6 4.8Z" />',
'solid' => '<path d="M7.17 6A5.001 5.001 0 0 0 3 11v7h7v-7H6.5a3 3 0 0 1 2.92-2.34L7.17 6Zm10 0A5.001 5.001 0 0 0 13 11v7h7v-7h-3.5a3 3 0 0 1 2.92-2.34L17.17 6Z" />',
];
@endphp
<x-ui.card
as="figure"
:variant="$variant === 'plain' ? 'ghost' : 'default'"
data-slot="quote-card"
data-variant="{{ $variant }}"
data-size="{{ $size }}"
{{ $attributes->merge(['class' => $figureClass]) }}
>
@if ($hasRating)
<div data-slot="quote-card-rating" class="flex">
<x-ui.rating display :value="(float) $rating" size="sm" />
</div>
@elseif ($glyph !== 'none')
<svg data-slot="quote-card-glyph" class="{{ $recipe['glyphs'][$glyph] ?? 'size-8' }}" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">{!! $glyphPaths[$glyph] !!}</svg>
@endif
<blockquote data-slot="quote-card-quote" class="{{ $quoteClass }}">@if ($quotes)“@endif{{ $quote ?? $slot }}@if ($quotes)”@endif</blockquote>
@if ($name !== null)
@if ($divider)
<x-ui.separator />
@endif
<figcaption data-slot="quote-card-caption" class="{{ $recipe['caption'] ?? '' }}">
<x-ui.avatar :size="$avatarSize" :src="$avatar" :alt="$name" :fallback="$fallback" />
<div class="min-w-0 text-start">
<p data-slot="quote-card-name" class="{{ $recipe['name'] ?? '' }}">{{ $name }}</p>
@if ($meta !== '')
<p data-slot="quote-card-meta" class="{{ $recipe['meta'] ?? '' }}">{{ $meta }}</p>
@endif
</div>
</figcaption>
@endif
</x-ui.card>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.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