Social cards 01
Four quote cards with the person, a verified mark and their company.
Last updated
---
/**
* Social cards 01: Four quote cards with the person, a verified mark and their company.
*
* @block testimonials
* @order 11
*/
import { Atom, BadgeCheck, Hexagon, Mountain, Sprout } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import SectionHeader from '@/components/apogee/SectionHeader/SectionHeader.astro'
import Avatar from '@/components/apogee/Avatar/Avatar.astro'
import { plain } from '@/components/apogee/sections/_shared/spine.ts'
import type { TestimonialsContent } from '@/components/apogee/sections/_shared/testimonials.ts'
/** @composable */
interface Props extends TestimonialsContent {}
const {
eyebrow,
heading = 'What our customers say',
description = 'Hear from the teams building with us every day.',
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', avatar: 'https://picsum.photos/id/1027/192/192', company: 'Lumen' },
{ quote: 'Every page we build now looks like it belongs together. Our designers finally trust what ships.', name: 'Daniel Reyes', role: 'Engineering Lead, Quanta', avatar: 'https://picsum.photos/id/91/192/192', company: 'Quanta' },
{ quote: 'Support answered in minutes, and the fix was live the same afternoon.', name: 'Sofia Marchetti', role: 'Product Designer, Fieldwork', avatar: 'https://picsum.photos/id/832/192/192', company: 'Fieldwork' },
{ quote: 'We tried building our own component library twice. This time we started from parts that were already accessible and tested, and spent our time on the product instead of on focus rings and contrast checks.', name: 'Kenji Watanabe', role: 'Founder, Northpeak', avatar: 'https://picsum.photos/id/856/192/192', company: 'Northpeak' },
],
} = Astro.props
// Company logo icons are design: they cycle by position, the text is the company.
const icons = [Hexagon, Atom, Sprout, Mountain]
---
<Section spacing="large">
<div class="flex flex-col gap-12 md:gap-16">
<SectionHeader eyebrow={eyebrow ?? undefined} heading={plain(heading)} description={description ?? undefined} size="large" align="center" />
<div class="grid gap-5 lg:grid-cols-2 lg:gap-6">
{items.map((t, i) => {
const Icon = icons[i % icons.length]!
return (
<figure class="flex flex-col gap-8 rounded-xl p-6 lg:min-h-64 lg:justify-between lg:p-8 bg-elevation-raised shadow-raised ring-1 ring-neutral">
{t.company && <span class="lg:hidden"><span class="flex items-center gap-2 text-lg font-semibold text-neutral"><Icon class="size-7" aria-hidden="true" />{t.company}</span></span>}
<blockquote class="text-lg font-medium lg:text-xl"><p>“{t.quote}”</p></blockquote>
<div class="flex items-center justify-between gap-4"><figcaption class="flex items-center gap-3"><Avatar src={t.avatar} name={t.name} alt="" size="large" border /><span class="flex flex-col"><span class="flex items-center gap-1 font-semibold">{t.name}<BadgeCheck class="size-4 text-primary" aria-label="Verified" /></span>{t.role && <span class="text-sm text-neutral-faded">{t.role}</span>}</span></figcaption>{t.company && <span class="max-lg:hidden"><span class="flex items-center gap-2 text-lg font-semibold text-neutral"><Icon class="size-7" aria-hidden="true" />{t.company}</span></span>}</div>
</figure>
)
})}
</div>
</div>
</Section>
Show codeHide code
---
/**
* Social cards 01: Four quote cards with the person, a verified mark and their company.
*
* @block testimonials
* @order 11
*/
import { Atom, BadgeCheck, Hexagon, Mountain, Sprout } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import SectionHeader from '@/components/apogee/SectionHeader/SectionHeader.astro'
import Avatar from '@/components/apogee/Avatar/Avatar.astro'
import { plain } from '@/components/apogee/sections/_shared/spine.ts'
import type { TestimonialsContent } from '@/components/apogee/sections/_shared/testimonials.ts'
/** @composable */
interface Props extends TestimonialsContent {}
const {
eyebrow,
heading = 'What our customers say',
description = 'Hear from the teams building with us every day.',
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', avatar: 'https://picsum.photos/id/1027/192/192', company: 'Lumen' },
{ quote: 'Every page we build now looks like it belongs together. Our designers finally trust what ships.', name: 'Daniel Reyes', role: 'Engineering Lead, Quanta', avatar: 'https://picsum.photos/id/91/192/192', company: 'Quanta' },
{ quote: 'Support answered in minutes, and the fix was live the same afternoon.', name: 'Sofia Marchetti', role: 'Product Designer, Fieldwork', avatar: 'https://picsum.photos/id/832/192/192', company: 'Fieldwork' },
{ quote: 'We tried building our own component library twice. This time we started from parts that were already accessible and tested, and spent our time on the product instead of on focus rings and contrast checks.', name: 'Kenji Watanabe', role: 'Founder, Northpeak', avatar: 'https://picsum.photos/id/856/192/192', company: 'Northpeak' },
],
} = Astro.props
// Company logo icons are design: they cycle by position, the text is the company.
const icons = [Hexagon, Atom, Sprout, Mountain]
---
<Section spacing="large">
<div class="flex flex-col gap-12 md:gap-16">
<SectionHeader eyebrow={eyebrow ?? undefined} heading={plain(heading)} description={description ?? undefined} size="large" align="center" />
<div class="grid gap-5 lg:grid-cols-2 lg:gap-6">
{items.map((t, i) => {
const Icon = icons[i % icons.length]!
return (
<figure class="flex flex-col gap-8 rounded-xl p-6 lg:min-h-64 lg:justify-between lg:p-8 bg-elevation-raised shadow-raised ring-1 ring-neutral">
{t.company && <span class="lg:hidden"><span class="flex items-center gap-2 text-lg font-semibold text-neutral"><Icon class="size-7" aria-hidden="true" />{t.company}</span></span>}
<blockquote class="text-lg font-medium lg:text-xl"><p>“{t.quote}”</p></blockquote>
<div class="flex items-center justify-between gap-4"><figcaption class="flex items-center gap-3"><Avatar src={t.avatar} name={t.name} alt="" size="large" border /><span class="flex flex-col"><span class="flex items-center gap-1 font-semibold">{t.name}<BadgeCheck class="size-4 text-primary" aria-label="Verified" /></span>{t.role && <span class="text-sm text-neutral-faded">{t.role}</span>}</span></figcaption>{t.company && <span class="max-lg:hidden"><span class="flex items-center gap-2 text-lg font-semibold text-neutral"><Icon class="size-7" aria-hidden="true" />{t.company}</span></span>}</div>
</figure>
)
})}
</div>
</div>
</Section>Add
npx @ava-ui/apogee add testimonials/social-cards-01
This copies the block to src/components/apogee/blocks/testimonials/ with everything it uses: Avatar, Section, SectionHeader. 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 heading is an
h2by default, so the block slots into a page under itsh1; item titles are one level down.
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.