Columns with links
A bar whose menus open across the page: three columns beside a list of quick links.
Last updated
---
/**
* Columns with links: A bar whose menus open across the page: three columns beside a list of quick links.
*
* @block header-navigation
* @order 14
*/
import { Book, CircleCheck, CircleDollarSign, CirclePlay, Flag, HeartHandshake, MessageCircle, Orbit, Sparkles, Users } 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 { NavGroup, NavigationContent } from '@/components/apogee/sections/_shared/navigation.ts'
// One icon set per column, cycling by position.
const icons = [
[CircleCheck, HeartHandshake, CircleDollarSign],
[Book, Sparkles, CirclePlay],
[Flag, MessageCircle, Users],
]
// A menu's last group is the quick-links list beside the columns.
const groups: NavGroup[] = [
{ heading: 'Use cases', items: [
{ label: 'Convert', href: '#', description: 'Read conversion rates, and raise them.' },
{ label: 'Engage', href: '#', description: 'Measure active use, and find what to improve.' },
{ label: 'Retain', href: '#', description: 'Find what keeps customers with you.' },
] },
{ 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: 'Company', items: [
{ label: 'About us', href: '#', description: 'Our team and what we are working towards.' },
{ label: 'Press', href: '#', description: 'News, press releases and press resources.' },
{ label: 'Careers', href: '#', description: 'We are always looking for talented people. Join our remote team.', badge: "We're hiring" },
] },
{ heading: 'Get started', items: [
{ label: 'Getting started', href: '#' },
{ label: 'Setup 101', href: '#' },
{ label: 'Community', href: '#' },
{ label: 'Changelog', href: '#' },
{ label: 'Tutorials', href: '#' },
{ label: 'Status', href: '#' },
] },
]
/** @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
}
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: '#' },
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 side = l.groups.at(-1)
return (
<NavDropdown label={l.label} fullWidth>
<div class="mx-auto max-w-7xl px-gutter py-6 md:py-8">
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_16rem]">
<div class="grid gap-4 md:grid-cols-3">
{l.groups.slice(0, -1).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 max-md:hidden">{i.description}</span>}
</span>
</Link>
</li>
)})}
</ul>
</div>
)})}
</div>
{side && <div class="flex flex-col gap-3 rounded-2xl bg-neutral-faded p-4">
{side.heading && <p class="text-sm font-semibold text-primary">{side.heading}</p>}
<ul class="flex list-none flex-col gap-3 p-0 font-semibold">
{side.items.map((s) => <li><Link href={s.href} color="neutral" underline="hover">{s.label}</Link></li>)}
</ul>
</div>}
</div>
</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
---
/**
* Columns with links: A bar whose menus open across the page: three columns beside a list of quick links.
*
* @block header-navigation
* @order 14
*/
import { Book, CircleCheck, CircleDollarSign, CirclePlay, Flag, HeartHandshake, MessageCircle, Orbit, Sparkles, Users } 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 { NavGroup, NavigationContent } from '@/components/apogee/sections/_shared/navigation.ts'
// One icon set per column, cycling by position.
const icons = [
[CircleCheck, HeartHandshake, CircleDollarSign],
[Book, Sparkles, CirclePlay],
[Flag, MessageCircle, Users],
]
// A menu's last group is the quick-links list beside the columns.
const groups: NavGroup[] = [
{ heading: 'Use cases', items: [
{ label: 'Convert', href: '#', description: 'Read conversion rates, and raise them.' },
{ label: 'Engage', href: '#', description: 'Measure active use, and find what to improve.' },
{ label: 'Retain', href: '#', description: 'Find what keeps customers with you.' },
] },
{ 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: 'Company', items: [
{ label: 'About us', href: '#', description: 'Our team and what we are working towards.' },
{ label: 'Press', href: '#', description: 'News, press releases and press resources.' },
{ label: 'Careers', href: '#', description: 'We are always looking for talented people. Join our remote team.', badge: "We're hiring" },
] },
{ heading: 'Get started', items: [
{ label: 'Getting started', href: '#' },
{ label: 'Setup 101', href: '#' },
{ label: 'Community', href: '#' },
{ label: 'Changelog', href: '#' },
{ label: 'Tutorials', href: '#' },
{ label: 'Status', href: '#' },
] },
]
/** @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
}
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: '#' },
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 side = l.groups.at(-1)
return (
<NavDropdown label={l.label} fullWidth>
<div class="mx-auto max-w-7xl px-gutter py-6 md:py-8">
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_16rem]">
<div class="grid gap-4 md:grid-cols-3">
{l.groups.slice(0, -1).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 max-md:hidden">{i.description}</span>}
</span>
</Link>
</li>
)})}
</ul>
</div>
)})}
</div>
{side && <div class="flex flex-col gap-3 rounded-2xl bg-neutral-faded p-4">
{side.heading && <p class="text-sm font-semibold text-primary">{side.heading}</p>}
<ul class="flex list-none flex-col gap-3 p-0 font-semibold">
{side.items.map((s) => <li><Link href={s.href} color="neutral" underline="hover">{s.label}</Link></li>)}
</ul>
</div>}
</div>
</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/two-col-with-links
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.