Nine quote cards in staggered columns that fade out at the bottom.

Last updated

Open block
Show code
---
/**
 * Social cards 03: Nine quote cards in staggered columns that fade out at the bottom.
 *
 * @block testimonials
 * @order 15
 */
import { Atom, BadgeCheck, Hexagon, Mountain, Sprout, Waves, Wind } 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' },
    { quote: 'The reports used to take a whole afternoon to put together. Now they are ready before our Monday meeting, and everyone reads the same numbers.', name: 'Leah Goldberg', role: 'Customer Success Lead, Arcwave', avatar: 'https://picsum.photos/id/823/192/192', company: 'Arcwave' },
    { quote: 'What sold us was the detail: keyboard support everywhere, dark mode that just works, and docs that answer the question you actually have.', name: 'Omar Haddad', role: 'CTO, Kitestring', avatar: 'https://picsum.photos/id/375/192/192', company: 'Kitestring' },
    { quote: 'It replaced four tools and a spreadsheet nobody wanted to open.', name: 'Ines Duarte', role: 'Marketing Director, Lumen', avatar: 'https://picsum.photos/id/64/192/192', company: 'Lumen' },
    { quote: 'Onboarding took a day. Our new hires were shipping by the end of their first week.', name: 'Tobias Brandt', role: 'Frontend Developer, Quanta', avatar: 'https://picsum.photos/id/669/192/192', company: 'Quanta' },
    { quote: 'We shipped our redesign in three weeks instead of three months. The team still talks about it.', name: 'Priya Nair', role: 'Head of Design, Fieldwork', avatar: 'https://picsum.photos/id/836/192/192', company: 'Fieldwork' },
  ],
} = Astro.props
// Company logo icons are design: they cycle by position, the text is the company.
const icons = [Hexagon, Atom, Sprout, Mountain, Waves, Wind]
---

<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="-mx-4 -mt-4 max-h-[61rem] gap-6 overflow-hidden px-4 pt-4 mask-b-from-70% lg:columns-3 max-md:[&>*:nth-child(n+4)]:hidden">
      {items.map((t, i) => {
        const Icon = icons[i % icons.length]!
        return (
      <figure class="mb-6 flex break-inside-avoid flex-col gap-6 rounded-xl p-6 lg:p-8 bg-elevation-raised shadow-raised ring-1 ring-neutral">
        {t.company && <span class="flex items-center gap-2 text-lg font-semibold text-neutral"><Icon class="size-7" aria-hidden="true" />{t.company}</span>}
        <blockquote class="text-neutral-faded"><p>“{t.quote}”</p></blockquote>
        <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>
      </figure>
        )
      })}
    </div>
  </div>
</Section>

Add

npx @ava-ui/apogee add testimonials/social-cards-03

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 h2 by default, so the block slots into a page under its h1; item titles are one level down.