Dropdown two columns with links and footer
A bar whose menus open an intro, quick links and two columns over two buttons.
Last updated
---
/**
* Dropdown two columns with links and footer: A bar whose menus open an intro, quick links and two columns over two buttons.
*
* @block header-navigation
* @order 7
*/
import { Book, BookOpen, CirclePlay, Code, FileCode, LifeBuoy, Orbit, Sparkles } 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 Badge from '@/components/apogee/Badge/Badge.astro'
import type { Action } from '@/components/apogee/sections/_shared/spine.ts'
import type { NavGroup, NavigationContent } from '@/components/apogee/sections/_shared/navigation.ts'
// One icon set per column, cycling by position.
const icons = [
[Book, Sparkles, CirclePlay],
[FileCode, LifeBuoy, Code],
]
// A menu's first group is the intro column of quick links; the rest are columns.
const groups: NavGroup[] = [
{ heading: 'Resources', items: [
{ label: 'Getting started', href: '#' },
{ label: 'Setup 101', href: '#' },
{ label: 'Community', href: '#' },
{ label: 'Changelog', href: '#' },
{ label: 'Tutorials', href: '#' },
{ label: 'Status', href: '#' },
] },
{ heading: 'Resources', items: [
{ label: 'Blog', href: '#', description: 'The latest industry news and guides, written by our team.' },
{ label: 'Customer stories', href: '#', description: 'How teams use the product to grow faster.' },
{ label: 'Video tutorials', href: '#', description: 'Get up and running on new features with short guides.' },
] },
{ heading: 'Support', items: [
{ label: 'Documentation', href: '#', description: 'In-depth articles on our tools and technologies.' },
{ label: 'Help and support', href: '#', description: 'Need help with something? Our team is here around the clock.' },
{ label: 'API reference', href: '#', description: 'Reference docs and helpful guides for our API.' },
] },
]
/** @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 line under the intro column's heading. @category content */
introDescription?: string | null
/** 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: '#' },
introDescription = 'Get started and learn more about our products.',
footerPrimaryAction = { label: 'View all resources', 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) => {
if (!('groups' in l)) return <NavLink href={l.href}>{l.label}</NavLink>
const [intro, ...cols] = l.groups
return (
<NavDropdown label={l.label} width="52rem">
<div class="grid gap-6 p-4 md:grid-cols-[12rem_minmax(0,1fr)_minmax(0,1fr)]">
{intro && <div class="flex flex-col gap-4 px-3">
{(intro.heading || introDescription) && <div class="flex flex-col gap-1">{intro.heading && <p class="font-semibold">{intro.heading}</p>}{introDescription && <p class="text-sm text-neutral-faded">{introDescription}</p>}</div>}
<ul class="flex list-none flex-col gap-3 p-0 text-sm font-semibold">
{intro.items.map((s) => <li><Link href={s.href} color="neutral" underline="hover">{s.label}</Link></li>)}
</ul>
</div>}
{cols.map((g, gi) => {
const set = icons[gi % icons.length]!
return (
<div class="flex flex-col gap-2">
{g.heading && <p class="px-3 text-sm font-semibold text-primary">{g.heading}</p>}
<ul class="list-none p-0 flex flex-col gap-0.5">
{g.items.map((i, n) => {
const Icon = set[n % set.length]!
return (
<li>
<Link href={i.href} color="inherit" underline="none" class="flex gap-3 rounded-lg p-3 transition-colors hover:bg-neutral-faded">
<Icon class="mt-0.5 size-5 shrink-0 text-primary" aria-hidden="true" />
<span class="flex flex-col gap-1">
<span class="flex flex-wrap items-center gap-2 font-semibold">{i.label}{i.badge && <Badge size="small" color={i.badge === 'New' ? 'neutral' : 'positive'} variant="faded">{i.badge}</Badge>}</span>
{i.description && <span class="line-clamp-2 text-sm text-neutral-faded">{i.description}</span>}
</span>
</Link>
</li>
)})}
</ul>
</div>
)})}
</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>
)
})}
{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
---
/**
* Dropdown two columns with links and footer: A bar whose menus open an intro, quick links and two columns over two buttons.
*
* @block header-navigation
* @order 7
*/
import { Book, BookOpen, CirclePlay, Code, FileCode, LifeBuoy, Orbit, Sparkles } 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 Badge from '@/components/apogee/Badge/Badge.astro'
import type { Action } from '@/components/apogee/sections/_shared/spine.ts'
import type { NavGroup, NavigationContent } from '@/components/apogee/sections/_shared/navigation.ts'
// One icon set per column, cycling by position.
const icons = [
[Book, Sparkles, CirclePlay],
[FileCode, LifeBuoy, Code],
]
// A menu's first group is the intro column of quick links; the rest are columns.
const groups: NavGroup[] = [
{ heading: 'Resources', items: [
{ label: 'Getting started', href: '#' },
{ label: 'Setup 101', href: '#' },
{ label: 'Community', href: '#' },
{ label: 'Changelog', href: '#' },
{ label: 'Tutorials', href: '#' },
{ label: 'Status', href: '#' },
] },
{ heading: 'Resources', items: [
{ label: 'Blog', href: '#', description: 'The latest industry news and guides, written by our team.' },
{ label: 'Customer stories', href: '#', description: 'How teams use the product to grow faster.' },
{ label: 'Video tutorials', href: '#', description: 'Get up and running on new features with short guides.' },
] },
{ heading: 'Support', items: [
{ label: 'Documentation', href: '#', description: 'In-depth articles on our tools and technologies.' },
{ label: 'Help and support', href: '#', description: 'Need help with something? Our team is here around the clock.' },
{ label: 'API reference', href: '#', description: 'Reference docs and helpful guides for our API.' },
] },
]
/** @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 line under the intro column's heading. @category content */
introDescription?: string | null
/** 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: '#' },
introDescription = 'Get started and learn more about our products.',
footerPrimaryAction = { label: 'View all resources', 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) => {
if (!('groups' in l)) return <NavLink href={l.href}>{l.label}</NavLink>
const [intro, ...cols] = l.groups
return (
<NavDropdown label={l.label} width="52rem">
<div class="grid gap-6 p-4 md:grid-cols-[12rem_minmax(0,1fr)_minmax(0,1fr)]">
{intro && <div class="flex flex-col gap-4 px-3">
{(intro.heading || introDescription) && <div class="flex flex-col gap-1">{intro.heading && <p class="font-semibold">{intro.heading}</p>}{introDescription && <p class="text-sm text-neutral-faded">{introDescription}</p>}</div>}
<ul class="flex list-none flex-col gap-3 p-0 text-sm font-semibold">
{intro.items.map((s) => <li><Link href={s.href} color="neutral" underline="hover">{s.label}</Link></li>)}
</ul>
</div>}
{cols.map((g, gi) => {
const set = icons[gi % icons.length]!
return (
<div class="flex flex-col gap-2">
{g.heading && <p class="px-3 text-sm font-semibold text-primary">{g.heading}</p>}
<ul class="list-none p-0 flex flex-col gap-0.5">
{g.items.map((i, n) => {
const Icon = set[n % set.length]!
return (
<li>
<Link href={i.href} color="inherit" underline="none" class="flex gap-3 rounded-lg p-3 transition-colors hover:bg-neutral-faded">
<Icon class="mt-0.5 size-5 shrink-0 text-primary" aria-hidden="true" />
<span class="flex flex-col gap-1">
<span class="flex flex-wrap items-center gap-2 font-semibold">{i.label}{i.badge && <Badge size="small" color={i.badge === 'New' ? 'neutral' : 'positive'} variant="faded">{i.badge}</Badge>}</span>
{i.description && <span class="line-clamp-2 text-sm text-neutral-faded">{i.description}</span>}
</span>
</Link>
</li>
)})}
</ul>
</div>
)})}
</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>
)
})}
{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/dropdown-two-cols-with-links-and-footer
This copies the block to src/components/apogee/blocks/header-navigation/ with everything it uses: Badge, Button, FeaturedIcon, 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.
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.