---
title: ChatWindowQuote
description: Displays one or more resort price quotes inside the chat message log. A single quote renders as a full inline card; two or more render as compact cards in a horizontal scroll zone.
---

```tsx
'use client';

import { ChatWindowQuote } from '@/ui/ChatWindowQuote';
import type { ChatWindowQuoteItem } from '@/ui/ChatWindowQuote';

const cancun: ChatWindowQuoteItem = {
  id: '1',
  resortName: 'Cancún',
  country: 'Mexico',
  imageUrl: 'https://picsum.photos/seed/cancun/400/300',
  dateRange: '15 June 2026 to 24 June 2026',
  pax: 'for 2 adults 2 children',
  packageIncludes: [
    'Meals and snacks at any time',
    'Drinks, alcoholic and non-alcoholic',
    'Sports and activities',
  ],
  additionalInfo: 'You can customise your transport and room category directly on the website.',
};

const puntaCana: ChatWindowQuoteItem = {
  id: '2',
  resortName: 'Punta Cana',
  country: 'Dominican Republic',
  imageUrl: 'https://picsum.photos/seed/puntacana/400/300',
  dateRange: '20 July 2026 to 29 July 2026',
  pax: 'for 2 adults 2 children',
};

export function ChatWindowQuoteDemo() {
  return (
    <div className="flex flex-col gap-32 w-340">
      <div className="flex flex-col gap-8">
        <p className="text-b4 font-semibold text-black">Single quote</p>
        <ChatWindowQuote quotes={[cancun]} />
      </div>
      <div className="flex flex-col gap-8">
        <p className="text-b4 font-semibold text-black">Multiple quotes</p>
        <ChatWindowQuote quotes={[cancun, puntaCana]} />
      </div>
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/chat-window-quote.json
```

Or manually, copy the following code into `/ui/ChatWindowQuote.tsx`:

```tsx
'use client';

import { twMerge } from '../helpers/twMerge';
import { Button } from '../buttons/Button';
import { Scrollbar } from '../Scrollbar';

export interface ChatWindowQuoteItem {
  id: string;
  resortName: string;
  country: string;
  imageUrl: string;
  /** Date range, e.g. "15 June 2026 to 24 June 2026" */
  dateRange: string;
  /** Pax info, e.g. "for 2 adults 2 children" */
  pax: string;
  /** CTA button label. Defaults to "See prices" */
  ctaLabel?: string;
  /** Bullet-point package items. Shown in single-quote inline mode only */
  packageIncludes?: string[];
  /** Free-text paragraph shown in single-quote inline mode only */
  additionalInfo?: string;
}

export interface ChatWindowQuoteProps {
  quotes: ChatWindowQuoteItem[];
  onCtaClick?: (quote: ChatWindowQuoteItem) => void;
  className?: string;
}

interface QuoteCardProps {
  quote: ChatWindowQuoteItem;
  onCtaClick?: (quote: ChatWindowQuoteItem) => void;
}

function SingleQuoteCard({ quote, onCtaClick }: QuoteCardProps) {
  return (
    <article
      aria-label={quote.resortName}
      className="flex flex-col bg-white border border-lightGrey rounded-32 shadow-[0px_2px_14px_0px_rgba(0,0,0,0.25)] overflow-hidden p-24 gap-16"
    >
      <div className="relative h-210 w-full overflow-hidden rounded-16">
        <img src={quote.imageUrl} alt="" className="absolute inset-0 size-full object-cover" />
        <div className="absolute inset-0 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]" />
        <div className="absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-20">
          <p className="text-b2 font-semibold">{quote.resortName}</p>
          <p className="text-b3">{quote.country}</p>
        </div>
      </div>
      <div className="flex flex-col gap-16 items-center">
        <p className="text-b4 font-semibold text-black text-center">
          {quote.resortName}, {quote.dateRange}, {quote.pax}
        </p>
        <Button color="saffron" onClick={() => onCtaClick?.(quote)}>
          {quote.ctaLabel ?? 'See prices'}
        </Button>
        {quote.packageIncludes && quote.packageIncludes.length > 0 && (
          <div className="flex flex-col gap-4 w-full">
            <p className="text-b3 font-semibold text-black">Your All-Inclusive package includes:</p>
            <ul className="flex flex-col gap-2">
              {quote.packageIncludes.map((item) => (
                <li key={item} className="text-b3 text-black">
                  • {item}
                </li>
              ))}
            </ul>
          </div>
        )}
        {quote.additionalInfo && <p className="text-b3 text-black">{quote.additionalInfo}</p>}
      </div>
    </article>
  );
}

function CompactQuoteCard({ quote, onCtaClick }: QuoteCardProps) {
  return (
    <article
      aria-label={quote.resortName}
      className="flex flex-col gap-12 items-center shrink-0 w-200"
    >
      <div className="relative size-200 rounded-16 overflow-hidden">
        <img src={quote.imageUrl} alt="" className="absolute inset-0 size-full object-cover" />
        <div className="absolute inset-0 rounded-16 bg-gradient-to-b from-transparent from-50% to-[rgba(36,36,36,0.4)]" />
        <div className="absolute bottom-20 inset-x-0 flex flex-col items-center text-white text-center px-8">
          <p className="text-b4 font-semibold">{quote.resortName}</p>
          <p className="text-b5">{quote.country}</p>
        </div>
      </div>
      <div className="flex flex-col gap-8 items-center">
        <p className="text-b5 font-semibold text-black text-center">
          {quote.resortName}
          <br />
          {quote.dateRange},
          <br />
          {quote.pax}
        </p>
        <Button color="black" onClick={() => onCtaClick?.(quote)}>
          {quote.ctaLabel ?? 'See prices'}
        </Button>
      </div>
    </article>
  );
}

export function ChatWindowQuote({ quotes, onCtaClick, className }: ChatWindowQuoteProps) {
  if (quotes.length === 0) return null;

  if (quotes.length === 1) {
    return (
      <div className={twMerge(className)}>
        <SingleQuoteCard quote={quotes[0]} onCtaClick={onCtaClick} />
      </div>
    );
  }

  return (
    <div className={twMerge('flex flex-col gap-8', className)}>
      <Scrollbar
        timelineName="--chat-quote-scroller"
        scrollerClassName="pb-24"
        scrollbarWidth="60%"
        aria-label="Quote options"
      >
        <div className="flex gap-12">
          {quotes.map((quote) => (
            <CompactQuoteCard key={quote.id} quote={quote} onCtaClick={onCtaClick} />
          ))}
        </div>
      </Scrollbar>
    </div>
  );
}
```

## Usage

`ChatWindowQuote` is used internally by `ChatWindow` — you rarely need to render it directly.
The standard entry point is `chatRef.current.showQuotes(quotes)`. See the [ChatWindow](/docs/components/chat-window) docs for the full integration pattern.

```tsx showLineNumbers
import { ChatWindowQuote } from '@/ui/ChatWindowQuote';
import type { ChatWindowQuoteItem } from '@/ui/ChatWindowQuote';

const quotes: ChatWindowQuoteItem[] = [
  {
    id: '1',
    resortName: 'Cancún',
    country: 'Mexico',
    imageUrl: 'https://example.com/cancun.jpg',
    dateRange: '15 June 2026 to 24 June 2026',
    pax: 'for 2 adults 2 children',
  },
];

export function Component() {
  return (
    <ChatWindowQuote
      quotes={quotes}
      onCtaClick={(quote) => console.log('Selected:', quote.resortName)}
    />
  );
}
```

## Behaviour

**Single quote** — renders as a full-width card with:

1. A resort image (with gradient overlay and name/country text in white).
2. Date range and pax summary below the image.
3. A saffron "See prices" CTA button.
4. Optional package includes bullet list and additional info paragraph.

**Multiple quotes** — renders as a horizontally scrollable row of compact square cards. Each card shows the resort image, name, dates, and a black "See prices" button. A scroll progress indicator appears below the row.

Each call to `showQuotes()` **appends** a new block to the message log — previous quote blocks remain visible above.

## API Reference

### ChatWindowQuote

| Prop         | Type                                    | Default     | Description                                                        |
| ------------ | --------------------------------------- | ----------- | ------------------------------------------------------------------ |
| `quotes`     | `ChatWindowQuoteItem[]`                 | —           | **Required.** One or more quote items to display.                  |
| `onCtaClick` | `(quote: ChatWindowQuoteItem) => void`  | `undefined` | Called with the selected quote when a "See prices" button is clicked. |
| `className`  | `string`                                | `undefined` | Additional CSS classes on the outer wrapper.                       |

### ChatWindowQuoteItem

| Field             | Type       | Default         | Description                                                                  |
| ----------------- | ---------- | --------------- | ---------------------------------------------------------------------------- |
| `id`              | `string`   | —               | **Required.** Unique identifier for the quote (used as React key).           |
| `resortName`      | `string`   | —               | **Required.** Name of the resort (e.g. `"Cancún"`).                         |
| `country`         | `string`   | —               | **Required.** Country of the resort (e.g. `"Mexico"`).                      |
| `imageUrl`        | `string`   | —               | **Required.** URL for the resort hero image.                                 |
| `dateRange`       | `string`   | —               | **Required.** Human-readable date range (e.g. `"15 June 2026 to 24 June 2026"`). |
| `pax`             | `string`   | —               | **Required.** Passenger summary (e.g. `"for 2 adults 2 children"`).         |
| `ctaLabel`        | `string`   | `"See prices"`  | Label for the CTA button.                                                    |
| `packageIncludes` | `string[]` | `undefined`     | Bullet-point list of package items. Shown in single-quote mode only.         |
| `additionalInfo`  | `string`   | `undefined`     | Free-text paragraph shown below package includes. Single-quote mode only.    |

## Notes

- `packageIncludes` and `additionalInfo` are silently ignored when more than one quote is passed — the compact scroll layout has no space for them.
- The resort image is treated as decorative (`alt=""`). The resort name and country are always visible as text overlay, satisfying screen-reader requirements.
- Each quote card carries `role="article"` with `aria-label` set to the resort name, so screen readers can navigate between results.
- The horizontal scroll zone uses the `Scrollbar` component, which provides keyboard-draggable scroll and a visible progress indicator.
