Blog posts with footer
A bar whose menus open across the page: nine blog posts over two buttons.
Last updated
---
/**
* Blog posts with footer: A bar whose menus open across the page: nine blog posts over two buttons.
*
* @block header-navigation
* @order 17
*/
import { BookOpen, Orbit } from '@lucide/astro'
import Navbar from '@/components/apogee/Navbar/Navbar.astro'
import NavLink from '@/components/apogee/NavLink/NavLink.astro'
import NavDropdown from '@/components/apogee/NavDropdown/NavDropdown.astro'
import Link from '@/components/apogee/Link/Link.astro'
import Button from '@/components/apogee/Button/Button.astro'
import FeaturedIcon from '@/components/apogee/FeaturedIcon/FeaturedIcon.astro'
import Image from '@/components/apogee/Image/Image.astro'
import type { Action } from '@/components/apogee/sections/_shared/spine.ts'
import type { NavGroup, NavigationContent } from '@/components/apogee/sections/_shared/navigation.ts'
const groups: NavGroup[] = [{ items: [
{ label: 'Auto layout explained', href: '#', description: 'How flexible layouts save hours on every screen.', media: { src: 'https://picsum.photos/seed/apogee-nav-1/288/176', alt: '' } },
{ label: 'Ways to level up your product design', href: '#', description: 'Small habits that make a big difference over a year.', media: { src: 'https://picsum.photos/seed/apogee-nav-2/288/176', alt: '' } },
{ label: 'Synthesise data with affinity diagrams', href: '#', description: 'Turn a wall of notes into findings your team trusts.', media: { src: 'https://picsum.photos/seed/apogee-nav-3/288/176', alt: '' } },
{ label: 'Streamline your user research', href: '#', description: 'Plan, run and share research in half the time.', media: { src: 'https://picsum.photos/seed/apogee-nav-4/288/176', alt: '' } },
{ label: 'Inclusive UX research', href: '#', description: 'Who you leave out shapes what you build.', media: { src: 'https://picsum.photos/seed/apogee-nav-5/288/176', alt: '' } },
{ label: 'The anatomy of a good story', href: '#', description: 'Why some product stories stick and others slide off.', media: { src: 'https://picsum.photos/seed/apogee-nav-6/288/176', alt: '' } },
{ label: 'Copy that converts', href: '#', description: 'Plain words, clear actions, fewer drop-offs.', media: { src: 'https://picsum.photos/seed/apogee-nav-7/288/176', alt: '' } },
{ label: 'A design presentation that lands', href: '#', description: 'Show the problem first, then the work.', media: { src: 'https://picsum.photos/seed/apogee-nav-8/288/176', alt: '' } },
{ label: 'Showing the value of design', href: '#', description: 'Numbers and stories that win the next budget.', media: { src: 'https://picsum.photos/seed/apogee-nav-9/288/176', alt: '' } },
] }]
/** @composable */
interface Props extends NavigationContent {
/** The bar's fill: the page, a grey band or the brand colour. */
background?: 'page' | 'neutral-faded' | 'primary' | undefined
/** `dark` sets the bar in the dark theme on a light page. */
theme?: 'dark' | undefined
/** The solid button under each menu. @category content */
footerPrimaryAction?: Action | null
/** The outline button under each menu. @category content */
footerSecondaryAction?: Action | null
}
const {
brand = 'Example',
links = [
{ label: 'Products', groups },
{ label: 'Services', groups },
{ label: 'Pricing', href: '#' },
{ label: 'Resources', groups },
{ label: 'About', href: '#' },
],
primaryAction = { label: 'Sign up', href: '#' },
secondaryAction = { label: 'Log in', href: '#' },
footerPrimaryAction = { label: 'View all posts', href: '#' },
footerSecondaryAction = { label: 'Documentation', href: '#' },
background,
theme,
} = Astro.props
---
<Navbar background={background} data-theme={theme}>
<Link slot="brand" href="/" color="inherit" underline="none" class="inline-flex items-center gap-2 text-lg font-semibold"><FeaturedIcon theme="solid" color="primary" size="small"><Orbit /></FeaturedIcon>{brand}</Link>
{links.map((l) => 'groups' in l ? (
<NavDropdown label={l.label} fullWidth>
<div class="mx-auto max-w-7xl px-gutter py-6 md:py-8">
<ul class="list-none p-0 grid gap-2 lg:grid-cols-2 xl:grid-cols-3">
{l.groups.flatMap((g) => g.items).map((p) => (
<li>
<Link href={p.href} color="inherit" underline="none" class="flex gap-4 rounded-lg p-2 transition-colors hover:bg-neutral-faded">
{p.media && <Image src={p.media.src} alt={p.media.alt} width={288} height={176} fit="cover" radius="md" class="h-[5.5rem] w-36 shrink-0" />}
<span class="flex flex-col gap-1">
<span class="line-clamp-2 font-semibold">{p.label}</span>
{p.description && <span class="line-clamp-2 text-sm text-neutral-faded">{p.description}</span>}
</span>
</Link>
</li>
))}
</ul>
</div>
{(footerPrimaryAction || footerSecondaryAction) && <div class="flex flex-col gap-3 border-t border-neutral bg-neutral-faded px-4 py-3 sm:flex-row sm:justify-end md:px-6">
{footerSecondaryAction && <Button href={footerSecondaryAction.href} variant="outline" color="neutral" size="small"><BookOpen slot="start" aria-hidden="true" />{footerSecondaryAction.label}</Button>}
{footerPrimaryAction && <Button href={footerPrimaryAction.href} size="small">{footerPrimaryAction.label}</Button>}
</div>}
</NavDropdown>
) : <NavLink href={l.href}>{l.label}</NavLink>)}
{secondaryAction && <Button slot="actions" href={secondaryAction.href} variant="outline" color="neutral" size="small">{secondaryAction.label}</Button>}
{primaryAction && <Button slot="actions" href={primaryAction.href} size="small">{primaryAction.label}</Button>}
</Navbar>
Show codeHide code
---
/**
* Blog posts with footer: A bar whose menus open across the page: nine blog posts over two buttons.
*
* @block header-navigation
* @order 17
*/
import { BookOpen, Orbit } from '@lucide/astro'
import Navbar from '@/components/apogee/Navbar/Navbar.astro'
import NavLink from '@/components/apogee/NavLink/NavLink.astro'
import NavDropdown from '@/components/apogee/NavDropdown/NavDropdown.astro'
import Link from '@/components/apogee/Link/Link.astro'
import Button from '@/components/apogee/Button/Button.astro'
import FeaturedIcon from '@/components/apogee/FeaturedIcon/FeaturedIcon.astro'
import Image from '@/components/apogee/Image/Image.astro'
import type { Action } from '@/components/apogee/sections/_shared/spine.ts'
import type { NavGroup, NavigationContent } from '@/components/apogee/sections/_shared/navigation.ts'
const groups: NavGroup[] = [{ items: [
{ label: 'Auto layout explained', href: '#', description: 'How flexible layouts save hours on every screen.', media: { src: 'https://picsum.photos/seed/apogee-nav-1/288/176', alt: '' } },
{ label: 'Ways to level up your product design', href: '#', description: 'Small habits that make a big difference over a year.', media: { src: 'https://picsum.photos/seed/apogee-nav-2/288/176', alt: '' } },
{ label: 'Synthesise data with affinity diagrams', href: '#', description: 'Turn a wall of notes into findings your team trusts.', media: { src: 'https://picsum.photos/seed/apogee-nav-3/288/176', alt: '' } },
{ label: 'Streamline your user research', href: '#', description: 'Plan, run and share research in half the time.', media: { src: 'https://picsum.photos/seed/apogee-nav-4/288/176', alt: '' } },
{ label: 'Inclusive UX research', href: '#', description: 'Who you leave out shapes what you build.', media: { src: 'https://picsum.photos/seed/apogee-nav-5/288/176', alt: '' } },
{ label: 'The anatomy of a good story', href: '#', description: 'Why some product stories stick and others slide off.', media: { src: 'https://picsum.photos/seed/apogee-nav-6/288/176', alt: '' } },
{ label: 'Copy that converts', href: '#', description: 'Plain words, clear actions, fewer drop-offs.', media: { src: 'https://picsum.photos/seed/apogee-nav-7/288/176', alt: '' } },
{ label: 'A design presentation that lands', href: '#', description: 'Show the problem first, then the work.', media: { src: 'https://picsum.photos/seed/apogee-nav-8/288/176', alt: '' } },
{ label: 'Showing the value of design', href: '#', description: 'Numbers and stories that win the next budget.', media: { src: 'https://picsum.photos/seed/apogee-nav-9/288/176', alt: '' } },
] }]
/** @composable */
interface Props extends NavigationContent {
/** The bar's fill: the page, a grey band or the brand colour. */
background?: 'page' | 'neutral-faded' | 'primary' | undefined
/** `dark` sets the bar in the dark theme on a light page. */
theme?: 'dark' | undefined
/** The solid button under each menu. @category content */
footerPrimaryAction?: Action | null
/** The outline button under each menu. @category content */
footerSecondaryAction?: Action | null
}
const {
brand = 'Example',
links = [
{ label: 'Products', groups },
{ label: 'Services', groups },
{ label: 'Pricing', href: '#' },
{ label: 'Resources', groups },
{ label: 'About', href: '#' },
],
primaryAction = { label: 'Sign up', href: '#' },
secondaryAction = { label: 'Log in', href: '#' },
footerPrimaryAction = { label: 'View all posts', href: '#' },
footerSecondaryAction = { label: 'Documentation', href: '#' },
background,
theme,
} = Astro.props
---
<Navbar background={background} data-theme={theme}>
<Link slot="brand" href="/" color="inherit" underline="none" class="inline-flex items-center gap-2 text-lg font-semibold"><FeaturedIcon theme="solid" color="primary" size="small"><Orbit /></FeaturedIcon>{brand}</Link>
{links.map((l) => 'groups' in l ? (
<NavDropdown label={l.label} fullWidth>
<div class="mx-auto max-w-7xl px-gutter py-6 md:py-8">
<ul class="list-none p-0 grid gap-2 lg:grid-cols-2 xl:grid-cols-3">
{l.groups.flatMap((g) => g.items).map((p) => (
<li>
<Link href={p.href} color="inherit" underline="none" class="flex gap-4 rounded-lg p-2 transition-colors hover:bg-neutral-faded">
{p.media && <Image src={p.media.src} alt={p.media.alt} width={288} height={176} fit="cover" radius="md" class="h-[5.5rem] w-36 shrink-0" />}
<span class="flex flex-col gap-1">
<span class="line-clamp-2 font-semibold">{p.label}</span>
{p.description && <span class="line-clamp-2 text-sm text-neutral-faded">{p.description}</span>}
</span>
</Link>
</li>
))}
</ul>
</div>
{(footerPrimaryAction || footerSecondaryAction) && <div class="flex flex-col gap-3 border-t border-neutral bg-neutral-faded px-4 py-3 sm:flex-row sm:justify-end md:px-6">
{footerSecondaryAction && <Button href={footerSecondaryAction.href} variant="outline" color="neutral" size="small"><BookOpen slot="start" aria-hidden="true" />{footerSecondaryAction.label}</Button>}
{footerPrimaryAction && <Button href={footerPrimaryAction.href} size="small">{footerPrimaryAction.label}</Button>}
</div>}
</NavDropdown>
) : <NavLink href={l.href}>{l.label}</NavLink>)}
{secondaryAction && <Button slot="actions" href={secondaryAction.href} variant="outline" color="neutral" size="small">{secondaryAction.label}</Button>}
{primaryAction && <Button slot="actions" href={primaryAction.href} size="small">{primaryAction.label}</Button>}
</Navbar>Add
npx @ava-ui/apogee add header-navigation/blog-posts-with-footer
This copies the block to src/components/apogee/blocks/header-navigation/ with everything it uses: Button, FeaturedIcon, Image, Link, NavDropdown, NavLink, Navbar. 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
- Each menu is a button with a native popover: Esc or a click outside closes it and focus returns to the button. On phones the menus are rows that expand inside the menu. No JS is needed for either.
- Photos that only decorate have an empty
alt; give yours one when it carries meaning.
Related
NavbarA responsive site header that collapses its links into a mobile panel below the md breakpoint.
NavDropdownA Navbar item that opens a panel of links; a popover under the button or across the header on desktop, a row that expands in the phone menu.
NavLinkA navigation link that auto-detects and styles the current route, with prefix matching, external links and a collapsible NavGroup.