Card field 01
A field of small image cards that grow toward the top corner and fade away from it, with a two-tone serif heading, line and two buttons in the opposite corner.
Last updated
---
/**
* Card field 01: A tight wall of image cards that fades away from the top corner, with a two-tone serif heading, line and two buttons in the opposite corner.
*
* @block hero
* @order 53
*/
import { ArrowRight } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import Heading from '@/components/apogee/Heading/Heading.astro'
import Button from '@/components/apogee/Button/Button.astro'
import { accentRuns, type HeroContent } from '@/components/apogee/sections/_shared/spine.ts'
/** @composable */
interface Props extends HeroContent {}
const {
heading = 'New ads that win, every week. *Picked by designers.*',
description = 'Ad creatives made with AI and chosen by senior designers. Stills, video and motion for fast-growing brands, tested every week.',
primaryAction = { label: 'Book a demo', href: '#' },
secondaryAction = { label: 'See how it works', href: '#' },
} = Astro.props
// Cards fill a 6 × 5 grid edge to edge, 4px apart.
const cols = 6
const ids = [21, 26, 29, 36, 42, 48, 54, 60, 64, 65, 70, 76, 82, 88, 96, 100, 103, 106, 110, 113, 119, 129, 133, 139, 145, 152, 160, 164, 177, 180]
const cards = ids.map((id, i) => ({ id, c: i % cols, r: Math.floor(i / cols) }))
---
<Section spacing="none" containerWidth="full" class="overflow-hidden">
<div class="relative min-h-[46rem] md:min-h-[52rem]">
<div class="pointer-events-none absolute inset-0 grid grid-cols-6 grid-rows-5 gap-1 p-1 mask-[radial-gradient(ellipse_at_85%_15%,black_20%,transparent_65%)] rtl:mask-[radial-gradient(ellipse_at_15%_15%,black_20%,transparent_65%)]" aria-hidden="true">
{cards.map((k) => (
<div class="relative overflow-hidden rounded-md" style={`grid-column: ${k.c + 1}; grid-row: ${k.r + 1}`}>
<img src={`https://picsum.photos/id/${k.id}/300/480`} alt="" loading="lazy" class="size-full object-cover" />
</div>
))}
</div>
<!-- Fade behind the text so it reads over the cards. -->
<div class="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,var(--background-page)_30%,transparent_65%)]" aria-hidden="true"></div>
<div class="absolute inset-x-0 bottom-0 flex flex-col gap-6 px-gutter pb-16 md:pb-24">
<Heading as="h1" size="display" class="max-w-3xl text-balance">{accentRuns(heading).map((r) => (r.accent ? <span class="text-neutral-faded">{r.text}</span> : r.text))}</Heading>
{description && <p class="max-w-xl text-lg md:text-xl text-neutral-faded">{description}</p>}
{(primaryAction || secondaryAction) && (
<div class="flex flex-col gap-3 sm:flex-row">
{primaryAction && <Button href={primaryAction.href} size="large" rounded><ArrowRight slot="start" class="rtl:-scale-x-100" aria-hidden="true" />{primaryAction.label}</Button>}
{secondaryAction && <Button href={secondaryAction.href} size="large" rounded variant="ghost" color="neutral">{secondaryAction.label}</Button>}
</div>
)}
</div>
</div>
</Section>
Show codeHide code
---
/**
* Card field 01: A tight wall of image cards that fades away from the top corner, with a two-tone serif heading, line and two buttons in the opposite corner.
*
* @block hero
* @order 53
*/
import { ArrowRight } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import Heading from '@/components/apogee/Heading/Heading.astro'
import Button from '@/components/apogee/Button/Button.astro'
import { accentRuns, type HeroContent } from '@/components/apogee/sections/_shared/spine.ts'
/** @composable */
interface Props extends HeroContent {}
const {
heading = 'New ads that win, every week. *Picked by designers.*',
description = 'Ad creatives made with AI and chosen by senior designers. Stills, video and motion for fast-growing brands, tested every week.',
primaryAction = { label: 'Book a demo', href: '#' },
secondaryAction = { label: 'See how it works', href: '#' },
} = Astro.props
// Cards fill a 6 × 5 grid edge to edge, 4px apart.
const cols = 6
const ids = [21, 26, 29, 36, 42, 48, 54, 60, 64, 65, 70, 76, 82, 88, 96, 100, 103, 106, 110, 113, 119, 129, 133, 139, 145, 152, 160, 164, 177, 180]
const cards = ids.map((id, i) => ({ id, c: i % cols, r: Math.floor(i / cols) }))
---
<Section spacing="none" containerWidth="full" class="overflow-hidden">
<div class="relative min-h-[46rem] md:min-h-[52rem]">
<div class="pointer-events-none absolute inset-0 grid grid-cols-6 grid-rows-5 gap-1 p-1 mask-[radial-gradient(ellipse_at_85%_15%,black_20%,transparent_65%)] rtl:mask-[radial-gradient(ellipse_at_15%_15%,black_20%,transparent_65%)]" aria-hidden="true">
{cards.map((k) => (
<div class="relative overflow-hidden rounded-md" style={`grid-column: ${k.c + 1}; grid-row: ${k.r + 1}`}>
<img src={`https://picsum.photos/id/${k.id}/300/480`} alt="" loading="lazy" class="size-full object-cover" />
</div>
))}
</div>
<!-- Fade behind the text so it reads over the cards. -->
<div class="pointer-events-none absolute inset-0 bg-[linear-gradient(to_top,var(--background-page)_30%,transparent_65%)]" aria-hidden="true"></div>
<div class="absolute inset-x-0 bottom-0 flex flex-col gap-6 px-gutter pb-16 md:pb-24">
<Heading as="h1" size="display" class="max-w-3xl text-balance">{accentRuns(heading).map((r) => (r.accent ? <span class="text-neutral-faded">{r.text}</span> : r.text))}</Heading>
{description && <p class="max-w-xl text-lg md:text-xl text-neutral-faded">{description}</p>}
{(primaryAction || secondaryAction) && (
<div class="flex flex-col gap-3 sm:flex-row">
{primaryAction && <Button href={primaryAction.href} size="large" rounded><ArrowRight slot="start" class="rtl:-scale-x-100" aria-hidden="true" />{primaryAction.label}</Button>}
{secondaryAction && <Button href={secondaryAction.href} size="large" rounded variant="ghost" color="neutral">{secondaryAction.label}</Button>}
</div>
)}
</div>
</div>
</Section>Add
npx @ava-ui/apogee add hero/card-field-01
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.