Skip to content
Brok UI

Loading…

No results

Quote Card

Open source

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.

Version
v1.0.2
Stability
stable
License
MIT
Related
Card
Avatar
Rating
Twitter Card

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.
AM Avery Mitchell

Avery Mitchell

Head of Design, Northwind

Variant
Size
previews.components.quote-card.default.blade.php 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')"
/>

Variant options

Card Current
Plain Current

Size options

Sm Current
Md Current
Lg Current

Installation

terminal
php artisan ui:add quote-card

Registry contract

php artisan ui:add quote-card 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/quote-card.blade.php
Registry dependencies
avatar card rating separator
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.

quote-card.md
# 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

glyphs.blade.php Blade
<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.blade.php Blade
<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>
plain.blade.php Blade
<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>
rating.blade.php Blade
<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')"
/>
sizes.blade.php Blade
<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

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
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.

quote-card quote-card-caption quote-card-glyph quote-card-meta quote-card-name quote-card-quote quote-card-rating

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

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.
  • 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
quote-card quote-card-glyph quote-card-rating quote-card-quote quote-card-caption quote-card-name quote-card-meta
Theming hooks
quote-card card

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
  • 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-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/quote-card.blade.php Blade
{{--
    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)&ldquo;@endif{{ $quote ?? $slot }}@if ($quotes)&rdquo;@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