Card split image brand
Rotating quotes in a brand-colour card beside a video of the customer.
Last updated
---
/**
* Card split image brand: Rotating quotes in a brand-colour card beside a video of the customer.
*
* @block testimonials
* @order 22
*/
import Section from '@/components/apogee/Section/Section.astro'
import Rating from '@/components/apogee/Rating/Rating.astro'
import Carousel from '@/components/apogee/Carousel/Carousel.astro'
import VideoPlayer from '@/components/apogee/VideoPlayer/VideoPlayer.astro'
import type { TestimonialsContent } from '@/components/apogee/sections/_shared/testimonials.ts'
/** @composable */
interface Props extends TestimonialsContent {
/** The customer video beside the quotes; `title` names it for screen readers. @category content */
video?: { src: string; title: string; poster?: string; captions?: string } | null
}
const {
label = 'Testimonials',
items = [
{ quote: 'We shipped our redesign in three weeks instead of three months. The team still talks about it.', name: 'Amara Nwosu', role: 'Head of Operations, Lumen', rating: 5 },
{ quote: 'Every page we build now looks like it belongs together. Our designers finally trust what ships.', name: 'Daniel Reyes', role: 'Engineering Lead, Quanta', rating: 5 },
{ quote: 'Support answered in minutes, and the fix was live the same afternoon.', name: 'Sofia Marchetti', role: 'Product Designer, Fieldwork', rating: 5 },
],
video = { src: '/media/app-tour.mp4', poster: 'https://picsum.photos/id/823/960/1080', captions: '/media/app-tour.vtt', title: 'Leah Goldberg on working with us' },
} = Astro.props
---
<Section spacing="large">
<div class="grid overflow-hidden rounded-2xl md:rounded-3xl lg:grid-cols-[1fr_auto] bg-primary text-on-primary">
<div class="min-w-0 p-6 md:p-8 lg:p-16">
<Carousel label={label} visibleItems={{ base: 1 }} controls={false} dots>
{items.map((t) => (
<figure class="flex flex-col gap-6">
{t.rating != null && <Rating value={t.rating} readonly size="small" color="warning" aria-label={`Rated ${t.rating} out of 5`} />}
<blockquote class="text-xl font-medium tracking-tight text-balance md:text-3xl"><p>“{t.quote}”</p></blockquote>
<figcaption class="flex flex-col gap-1"><span class="flex flex-col"><span class="flex items-center gap-1 font-semibold">— {t.name}</span>{t.role && <span class="text-sm opacity-85">{t.role}</span>}</span></figcaption>
</figure>
))}
</Carousel>
</div>
{video && <VideoPlayer src={video.src} poster={video.poster} captions={video.captions} title={video.title} ratio="1/1" radius="none" class="border-0 lg:w-120" />}
</div>
</Section>
Show codeHide code
---
/**
* Card split image brand: Rotating quotes in a brand-colour card beside a video of the customer.
*
* @block testimonials
* @order 22
*/
import Section from '@/components/apogee/Section/Section.astro'
import Rating from '@/components/apogee/Rating/Rating.astro'
import Carousel from '@/components/apogee/Carousel/Carousel.astro'
import VideoPlayer from '@/components/apogee/VideoPlayer/VideoPlayer.astro'
import type { TestimonialsContent } from '@/components/apogee/sections/_shared/testimonials.ts'
/** @composable */
interface Props extends TestimonialsContent {
/** The customer video beside the quotes; `title` names it for screen readers. @category content */
video?: { src: string; title: string; poster?: string; captions?: string } | null
}
const {
label = 'Testimonials',
items = [
{ quote: 'We shipped our redesign in three weeks instead of three months. The team still talks about it.', name: 'Amara Nwosu', role: 'Head of Operations, Lumen', rating: 5 },
{ quote: 'Every page we build now looks like it belongs together. Our designers finally trust what ships.', name: 'Daniel Reyes', role: 'Engineering Lead, Quanta', rating: 5 },
{ quote: 'Support answered in minutes, and the fix was live the same afternoon.', name: 'Sofia Marchetti', role: 'Product Designer, Fieldwork', rating: 5 },
],
video = { src: '/media/app-tour.mp4', poster: 'https://picsum.photos/id/823/960/1080', captions: '/media/app-tour.vtt', title: 'Leah Goldberg on working with us' },
} = Astro.props
---
<Section spacing="large">
<div class="grid overflow-hidden rounded-2xl md:rounded-3xl lg:grid-cols-[1fr_auto] bg-primary text-on-primary">
<div class="min-w-0 p-6 md:p-8 lg:p-16">
<Carousel label={label} visibleItems={{ base: 1 }} controls={false} dots>
{items.map((t) => (
<figure class="flex flex-col gap-6">
{t.rating != null && <Rating value={t.rating} readonly size="small" color="warning" aria-label={`Rated ${t.rating} out of 5`} />}
<blockquote class="text-xl font-medium tracking-tight text-balance md:text-3xl"><p>“{t.quote}”</p></blockquote>
<figcaption class="flex flex-col gap-1"><span class="flex flex-col"><span class="flex items-center gap-1 font-semibold">— {t.name}</span>{t.role && <span class="text-sm opacity-85">{t.role}</span>}</span></figcaption>
</figure>
))}
</Carousel>
</div>
{video && <VideoPlayer src={video.src} poster={video.poster} captions={video.captions} title={video.title} ratio="1/1" radius="none" class="border-0 lg:w-120" />}
</div>
</Section>Add
npx @ava-ui/apogee add testimonials/card-split-image-brand
This copies the block to src/components/apogee/blocks/testimonials/ with everything it uses: Carousel, Rating, Section, VideoPlayer. Every piece of copy is a prop, with the sample copy as its default. Pass yours, or null to hide a part; see copy is props.
Accessibility
- The play button is named after the video; once it plays, the browser’s own controls take over. The video has captions.
- The stars are read as one rating (“Rated 5 out of 5”).
- The carousel is a named region; its arrows are buttons and it scrolls by swipe or keyboard too.
Related
TestimonialsOne featured quote, or a grid of quote cards.
CarouselA horizontal scroll-snap track for browsing slides. Native scroll and swipe work with zero JS; arrows, dots, and autoplay are progressive enhancement.
RatingA star rating on native radios — pick a value on a scale, or display one read-only.