Photo links 01
A row of four promises, a row of six black-and-white photo links with captions, then small link groups, a newsletter line, a centred wordmark and the copyright.
Last updated
---
/**
* Photo links 01: A row of four promises, a row of six black-and-white photo links with captions, then small link groups, a newsletter line, a centred wordmark and the copyright.
*
* @block footers
* @order 47
*/
import Section from '@/components/apogee/Section/Section.astro'
import Link from '@/components/apogee/Link/Link.astro'
import { type Action } from '@/components/apogee/sections/_shared/spine.ts'
import { type FooterContent } from '@/components/apogee/sections/_shared/footers.ts'
/** @composable */
interface Props extends FooterContent {
/** The row of short promises along the top. @category content */
promises?: { title: string; description: string }[] | null
/** Photo links with captions; `src` is the photo. @category content */
photos?: (Action & { src: string })[] | null
}
const {
brand = 'Hollis Ward',
newsletterHeading = 'Newsletter',
newsletterDescription = 'Sign up and get 10% off your first order.',
submitLabel = 'Submit',
columns = [
{ heading: 'Support', links: [{ label: 'Legal', href: '#' }, { label: 'Questions', href: '#' }, { label: 'Contact', href: '#' }, { label: 'Book a fitting', href: '#' }] },
{ heading: 'Company', links: [{ label: 'Contact', href: '#' }, { label: 'About', href: '#' }, { label: 'Journal', href: '#' }] },
],
copyright = '© 2026 Hollis Ward',
legal = [{ label: 'Terms and privacy', href: '#' }, { label: 'Credits', href: '#' }],
promises = [
{ title: 'Free shipping', description: 'on orders over $200 at home.' },
{ title: 'Made locally', description: 'with fabrics from mills we know by name.' },
{ title: 'Sign up', description: 'and get 10% off your first order.' },
{ title: 'Secure payments', description: 'with every major card and wallet.' },
],
photos = [
{ label: 'Shop all', href: '#', src: 'https://picsum.photos/id/1005/320/400?grayscale' },
{ label: 'Journal', href: '#', src: 'https://picsum.photos/id/1018/320/400?grayscale' },
{ label: 'Materials', href: '#', src: 'https://picsum.photos/id/1031/320/400?grayscale' },
{ label: 'Support', href: '#', src: 'https://picsum.photos/id/1048/320/400?grayscale' },
{ label: 'About', href: '#', src: 'https://picsum.photos/id/1062/320/400?grayscale' },
{ label: 'Contact', href: '#', src: 'https://picsum.photos/id/1067/320/400?grayscale' },
],
} = Astro.props
---
<Section as="footer" spacing="medium" containerWidth="full" class="text-sm">
{promises && <ul class="grid list-none gap-6 p-0 sm:grid-cols-2 lg:grid-cols-4">
{promises.map((p) => <li class="grid grid-cols-[8rem_1fr] gap-3"><span class="uppercase">{p.title}</span><span class="text-neutral-faded">{p.description}</span></li>)}
</ul>}
{photos && <ul class="mx-auto mt-16 grid max-w-5xl list-none grid-cols-3 gap-3 p-0 md:grid-cols-6">
{photos.map((p) => (
<li><Link href={p.href} color="neutral" underline="hover" class="group flex flex-col gap-2">
<img src={p.src} alt="" loading="lazy" class="aspect-4/5 w-full object-cover opacity-80 transition-opacity duration-(--duration-fast) group-hover:opacity-100" />{p.label}
</Link></li>
))}
</ul>}
<div class="mt-16 grid items-start gap-10 md:grid-cols-2 lg:grid-cols-[1fr_1fr_auto_1fr_1fr]">
{columns.slice(0, 1).map((c) => <nav aria-label={c.heading} class="grid grid-cols-[5rem_1fr] gap-3"><span class="uppercase">{c.heading}</span><ul class="flex list-none flex-col p-0">{c.links.map((l) => <li><Link href={l.href} color="neutral" underline="hover">{l.label}</Link></li>)}</ul></nav>)}
<form class="flex max-w-xs flex-col gap-1" onsubmit="return false">
{newsletterHeading && <span class="uppercase">{newsletterHeading}</span>}
{newsletterDescription && <span class="text-neutral-faded">{newsletterDescription}</span>}
<span class="mt-2 flex items-end gap-3">
<label for="photo-links-email" class="sr-only">Email</label>
<input id="photo-links-email" type="email" placeholder="Email" autocomplete="email" class="min-w-0 flex-1 border-b border-neutral bg-transparent py-1 outline-none placeholder:text-neutral-faded focus-visible:border-primary" />
<button type="submit" class="underline underline-offset-4">{submitLabel}</button>
</span>
</form>
<p class="text-3xl tracking-tight md:col-span-2 lg:col-span-1 lg:justify-self-center">{brand}</p>
{columns.slice(1).map((c) => <nav aria-label={c.heading} class="grid grid-cols-[5rem_1fr] gap-3"><span class="uppercase">{c.heading}</span><ul class="flex list-none flex-col p-0">{c.links.map((l) => <li><Link href={l.href} color="neutral" underline="hover">{l.label}</Link></li>)}</ul></nav>)}
<div class="flex flex-col gap-6"><ul class="flex list-none flex-col p-0"><li>Currency: <Link href="#" color="neutral">USD</Link></li>{legal && legal.map((l) => <li><Link href={l.href} color="neutral" underline="hover">{l.label}</Link></li>)}</ul>{copyright && <p class="text-neutral-faded">{copyright}</p>}</div>
</div>
</Section>
Show codeHide code
---
/**
* Photo links 01: A row of four promises, a row of six black-and-white photo links with captions, then small link groups, a newsletter line, a centred wordmark and the copyright.
*
* @block footers
* @order 47
*/
import Section from '@/components/apogee/Section/Section.astro'
import Link from '@/components/apogee/Link/Link.astro'
import { type Action } from '@/components/apogee/sections/_shared/spine.ts'
import { type FooterContent } from '@/components/apogee/sections/_shared/footers.ts'
/** @composable */
interface Props extends FooterContent {
/** The row of short promises along the top. @category content */
promises?: { title: string; description: string }[] | null
/** Photo links with captions; `src` is the photo. @category content */
photos?: (Action & { src: string })[] | null
}
const {
brand = 'Hollis Ward',
newsletterHeading = 'Newsletter',
newsletterDescription = 'Sign up and get 10% off your first order.',
submitLabel = 'Submit',
columns = [
{ heading: 'Support', links: [{ label: 'Legal', href: '#' }, { label: 'Questions', href: '#' }, { label: 'Contact', href: '#' }, { label: 'Book a fitting', href: '#' }] },
{ heading: 'Company', links: [{ label: 'Contact', href: '#' }, { label: 'About', href: '#' }, { label: 'Journal', href: '#' }] },
],
copyright = '© 2026 Hollis Ward',
legal = [{ label: 'Terms and privacy', href: '#' }, { label: 'Credits', href: '#' }],
promises = [
{ title: 'Free shipping', description: 'on orders over $200 at home.' },
{ title: 'Made locally', description: 'with fabrics from mills we know by name.' },
{ title: 'Sign up', description: 'and get 10% off your first order.' },
{ title: 'Secure payments', description: 'with every major card and wallet.' },
],
photos = [
{ label: 'Shop all', href: '#', src: 'https://picsum.photos/id/1005/320/400?grayscale' },
{ label: 'Journal', href: '#', src: 'https://picsum.photos/id/1018/320/400?grayscale' },
{ label: 'Materials', href: '#', src: 'https://picsum.photos/id/1031/320/400?grayscale' },
{ label: 'Support', href: '#', src: 'https://picsum.photos/id/1048/320/400?grayscale' },
{ label: 'About', href: '#', src: 'https://picsum.photos/id/1062/320/400?grayscale' },
{ label: 'Contact', href: '#', src: 'https://picsum.photos/id/1067/320/400?grayscale' },
],
} = Astro.props
---
<Section as="footer" spacing="medium" containerWidth="full" class="text-sm">
{promises && <ul class="grid list-none gap-6 p-0 sm:grid-cols-2 lg:grid-cols-4">
{promises.map((p) => <li class="grid grid-cols-[8rem_1fr] gap-3"><span class="uppercase">{p.title}</span><span class="text-neutral-faded">{p.description}</span></li>)}
</ul>}
{photos && <ul class="mx-auto mt-16 grid max-w-5xl list-none grid-cols-3 gap-3 p-0 md:grid-cols-6">
{photos.map((p) => (
<li><Link href={p.href} color="neutral" underline="hover" class="group flex flex-col gap-2">
<img src={p.src} alt="" loading="lazy" class="aspect-4/5 w-full object-cover opacity-80 transition-opacity duration-(--duration-fast) group-hover:opacity-100" />{p.label}
</Link></li>
))}
</ul>}
<div class="mt-16 grid items-start gap-10 md:grid-cols-2 lg:grid-cols-[1fr_1fr_auto_1fr_1fr]">
{columns.slice(0, 1).map((c) => <nav aria-label={c.heading} class="grid grid-cols-[5rem_1fr] gap-3"><span class="uppercase">{c.heading}</span><ul class="flex list-none flex-col p-0">{c.links.map((l) => <li><Link href={l.href} color="neutral" underline="hover">{l.label}</Link></li>)}</ul></nav>)}
<form class="flex max-w-xs flex-col gap-1" onsubmit="return false">
{newsletterHeading && <span class="uppercase">{newsletterHeading}</span>}
{newsletterDescription && <span class="text-neutral-faded">{newsletterDescription}</span>}
<span class="mt-2 flex items-end gap-3">
<label for="photo-links-email" class="sr-only">Email</label>
<input id="photo-links-email" type="email" placeholder="Email" autocomplete="email" class="min-w-0 flex-1 border-b border-neutral bg-transparent py-1 outline-none placeholder:text-neutral-faded focus-visible:border-primary" />
<button type="submit" class="underline underline-offset-4">{submitLabel}</button>
</span>
</form>
<p class="text-3xl tracking-tight md:col-span-2 lg:col-span-1 lg:justify-self-center">{brand}</p>
{columns.slice(1).map((c) => <nav aria-label={c.heading} class="grid grid-cols-[5rem_1fr] gap-3"><span class="uppercase">{c.heading}</span><ul class="flex list-none flex-col p-0">{c.links.map((l) => <li><Link href={l.href} color="neutral" underline="hover">{l.label}</Link></li>)}</ul></nav>)}
<div class="flex flex-col gap-6"><ul class="flex list-none flex-col p-0"><li>Currency: <Link href="#" color="neutral">USD</Link></li>{legal && legal.map((l) => <li><Link href={l.href} color="neutral" underline="hover">{l.label}</Link></li>)}</ul>{copyright && <p class="text-neutral-faded">{copyright}</p>}</div>
</div>
</Section>Add
npx @ava-ui/apogee add footers/photo-links-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.