Glassmorphic cards 03
A heading with actions over six portraits with glass panels, and a load-more button on phones.
Last updated
---
/**
* Glassmorphic cards 03: A heading with actions over six portraits with glass panels, and a load-more button on phones.
*
* @block testimonials
* @order 20
*/
import { ArrowDown } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import SectionHeader from '@/components/apogee/SectionHeader/SectionHeader.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Image from '@/components/apogee/Image/Image.astro'
import Rating from '@/components/apogee/Rating/Rating.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 {}
// A quote is optional on these cards: an empty `quote` shows the person only.
const {
eyebrow,
heading = 'What our customers say',
description = 'Hear from the teams building with us every day.',
primaryAction = { label: 'Create account', href: '#' },
secondaryAction = { label: 'Our customers', href: '#' },
items = [
{ quote: '', name: 'Amara Nwosu', role: 'Head of Operations, Lumen', avatar: 'https://picsum.photos/id/1027/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Daniel Reyes', role: 'Engineering Lead, Quanta', avatar: 'https://picsum.photos/id/91/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: 'Clear, calm and fast to work with.', name: 'Sofia Marchetti', role: 'Product Designer, Fieldwork', avatar: 'https://picsum.photos/id/832/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Kenji Watanabe', role: 'Founder, Northpeak', avatar: 'https://picsum.photos/id/856/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Leah Goldberg', role: 'Customer Success Lead, Arcwave', avatar: 'https://picsum.photos/id/823/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Omar Haddad', role: 'CTO, Kitestring', avatar: 'https://picsum.photos/id/375/800/1000', rating: 5, industry: 'Retail and commerce' },
],
} = Astro.props
---
<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">
{(primaryAction || secondaryAction) && <Fragment slot="actions"><div class="flex flex-col-reverse gap-3 sm:flex-row">{secondaryAction && <Button href={secondaryAction.href} size="large" variant="outline" color="neutral">{secondaryAction.label}</Button>}{primaryAction && <Button href={primaryAction.href} size="large">{primaryAction.label}</Button>}</div></Fragment>}
</SectionHeader>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{items.map((t) => (
<figure class="relative isolate flex h-96 flex-col justify-end overflow-hidden md:h-120">
{t.avatar && <div class="absolute inset-0 -z-10"><Image src={t.avatar} alt="" width={800} height={1000} fit="cover" class="size-full" /></div>}
<figcaption class="flex flex-col gap-3 m-3 rounded-xl bg-page/80 p-5 ring-1 ring-inset ring-neutral backdrop-blur-md md:m-4 md:rounded-2xl">
{t.quote && <blockquote class="text-xl font-semibold"><p>“{t.quote}”</p></blockquote>}
{t.rating != null && <Rating value={t.rating} readonly size="small" color="warning" aria-label={`Rated ${t.rating} out of 5`} />}
<span class="text-xl font-semibold md:text-2xl">{t.name}</span>
{(t.role || t.industry) && <span class="text-sm">{t.role && <span class="font-semibold">{t.role}</span>}{t.role && t.industry && <br />}{t.industry && <span class="text-neutral-faded">{t.industry}</span>}</span>}
</figcaption>
</figure>
))}
</div>
<Button href="?page=2" size="large" variant="outline" color="neutral" class="self-center md:hidden"><ArrowDown slot="start" aria-hidden="true" />Load more</Button>
</div>
</Section>
Show codeHide code
---
/**
* Glassmorphic cards 03: A heading with actions over six portraits with glass panels, and a load-more button on phones.
*
* @block testimonials
* @order 20
*/
import { ArrowDown } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import SectionHeader from '@/components/apogee/SectionHeader/SectionHeader.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Image from '@/components/apogee/Image/Image.astro'
import Rating from '@/components/apogee/Rating/Rating.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 {}
// A quote is optional on these cards: an empty `quote` shows the person only.
const {
eyebrow,
heading = 'What our customers say',
description = 'Hear from the teams building with us every day.',
primaryAction = { label: 'Create account', href: '#' },
secondaryAction = { label: 'Our customers', href: '#' },
items = [
{ quote: '', name: 'Amara Nwosu', role: 'Head of Operations, Lumen', avatar: 'https://picsum.photos/id/1027/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Daniel Reyes', role: 'Engineering Lead, Quanta', avatar: 'https://picsum.photos/id/91/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: 'Clear, calm and fast to work with.', name: 'Sofia Marchetti', role: 'Product Designer, Fieldwork', avatar: 'https://picsum.photos/id/832/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Kenji Watanabe', role: 'Founder, Northpeak', avatar: 'https://picsum.photos/id/856/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Leah Goldberg', role: 'Customer Success Lead, Arcwave', avatar: 'https://picsum.photos/id/823/800/1000', rating: 5, industry: 'Retail and commerce' },
{ quote: '', name: 'Omar Haddad', role: 'CTO, Kitestring', avatar: 'https://picsum.photos/id/375/800/1000', rating: 5, industry: 'Retail and commerce' },
],
} = Astro.props
---
<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">
{(primaryAction || secondaryAction) && <Fragment slot="actions"><div class="flex flex-col-reverse gap-3 sm:flex-row">{secondaryAction && <Button href={secondaryAction.href} size="large" variant="outline" color="neutral">{secondaryAction.label}</Button>}{primaryAction && <Button href={primaryAction.href} size="large">{primaryAction.label}</Button>}</div></Fragment>}
</SectionHeader>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{items.map((t) => (
<figure class="relative isolate flex h-96 flex-col justify-end overflow-hidden md:h-120">
{t.avatar && <div class="absolute inset-0 -z-10"><Image src={t.avatar} alt="" width={800} height={1000} fit="cover" class="size-full" /></div>}
<figcaption class="flex flex-col gap-3 m-3 rounded-xl bg-page/80 p-5 ring-1 ring-inset ring-neutral backdrop-blur-md md:m-4 md:rounded-2xl">
{t.quote && <blockquote class="text-xl font-semibold"><p>“{t.quote}”</p></blockquote>}
{t.rating != null && <Rating value={t.rating} readonly size="small" color="warning" aria-label={`Rated ${t.rating} out of 5`} />}
<span class="text-xl font-semibold md:text-2xl">{t.name}</span>
{(t.role || t.industry) && <span class="text-sm">{t.role && <span class="font-semibold">{t.role}</span>}{t.role && t.industry && <br />}{t.industry && <span class="text-neutral-faded">{t.industry}</span>}</span>}
</figcaption>
</figure>
))}
</div>
<Button href="?page=2" size="large" variant="outline" color="neutral" class="self-center md:hidden"><ArrowDown slot="start" aria-hidden="true" />Load more</Button>
</div>
</Section>Add
npx @ava-ui/apogee add testimonials/glassmorphic-cards-03
This copies the block to src/components/apogee/blocks/testimonials/ with everything it uses: Button, Image, Rating, 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. - The stars are read as one rating (“Rated 5 out of 5”).
- Photos that only decorate have an empty
alt; give yours one when it carries meaning.
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.