Skip to content
Download ZIP

Neutral palette · 12.3 KB ZIP File receipt View as Markdown View code

Preview

Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.

Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
cmp_dropdown_nav_01 · version 1.0.0 · Neutral palette
Using the PageSugar MCP server, fetch component cmp_dropdown_nav_01 version 1.0.0 with variant "neutral", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.

Not connected yet? Set up the MCP server

Code

Palette
Neutral
Version
1.0.0
Digest
Full digest
sha256-541b4f7ecf677b37d99fc4c714fb9008857f05c324e94c52118c0aebda922756
types.ts TypeScript · 2.3 KB Raw
/** A destination: a top-level link, or a row inside a group's panel. */
export interface NavLink {
	label: string;
	href: string;
	/** One line under the label, shown inside a panel only. */
	description?: string;
}

/** A top-level item that opens a panel of related links. */
export interface NavGroup {
	label: string;
	/** The section's own landing page, listed first in the panel as the overview link. */
	href?: string;
	/** One line under the overview link. */
	description?: string;
	/** Optional stable key, for a panelFooter that switches on the group rather than its label. */
	id?: string;
	children: NavLink[];
}

export type NavItem = NavLink | NavGroup;

/** Why a panel is open: a panel the pointer opened closes when it leaves; click and key panels stay. */
export type OpenReason = 'click' | 'key' | 'hover';

export const isGroup = (item: NavItem): item is NavGroup =>
	'children' in item && Array.isArray(item.children);

/**
 * Items as rendered. A link or group with a blank label has no accessible name and is dropped;
 * a group with no children becomes a plain link when it has an href and is dropped otherwise,
 * so nothing renders as a button over an empty panel.
 */
export const renderableItems = (items: NavItem[]): NavItem[] =>
	items.flatMap((item): NavItem[] => {
		if (item.label.trim() === '') return [];
		if (!isGroup(item)) return [item];
		const children = item.children.filter((child) => child.label.trim() !== '');
		if (children.length > 0) return [{ ...item, children }];
		return item.href ? [{ label: item.label, href: item.href }] : [];
	});

/** Pathname only: no query, no hash, no trailing slash (except the root). */
export const normalizePath = (path: string): string => {
	const bare = path.split(/[?#]/, 1)[0];
	return bare.length > 1 ? bare.replace(/\/+$/, '') : bare;
};

/** True when an href names the current pathname, ignoring query, hash and a trailing slash. */
export const isCurrentPath = (href: string | undefined, currentPath: string | undefined) =>
	Boolean(href && currentPath) && normalizePath(href!) === normalizePath(currentPath!);

/** True when the group's overview page or one of its children is the current page. */
export const isCurrentGroup = (group: NavGroup, currentPath: string | undefined) =>
	isCurrentPath(group.href, currentPath) ||
	group.children.some((child) => isCurrentPath(child.href, currentPath));

Pass top-level links and groups, and the current pathname. Each group is a button that opens a single-column panel of its children; a group with an href gets an overview link as its first row. It does not render nested submenus, multi-column mega-menu panels or a mobile drawer: pair it with a drawer or accordion below your breakpoint.

Suggested location
src/lib/components/dropdown-nav-01
Required props
items

Limitations

  • One level only: a group's children are links, never groups of their own.
  • Panels need JavaScript to open. Before hydration, or without it, group buttons do nothing and only top-level links and the overview pages you link elsewhere are reachable; link each section's page from somewhere else on the site too.
  • The row wraps rather than collapsing into a menu button; at phone widths show a drawer or accordion instead.
  • currentPath is compared as a pathname (query, hash and a trailing slash are ignored) and only exactly; a group is the current section when its own href or one of its children matches.
  • Items and children with a blank label are skipped, and a group with no children renders as a plain link when it has an href, or not at all.
  • Renders nothing when items is empty.
  • Dark colours apply inside an ancestor with the class dark; a media-query setup needs its own rule that sets the --dropdown-nav-* variables.
  • The open panel follows its group by id (or label when there is no id); if items change so that a different group sits in its place, the panel closes.

Example

Svelte
<!-- Illustrative content: replace the links with your own routes. -->
<script lang="ts">
	import { page } from '$app/state';
	import DropdownNav from '$lib/components/dropdown-nav-01/DropdownNav.svelte';
	import type { NavItem } from '$lib/components/dropdown-nav-01/types';

	const items: NavItem[] = [
		{
			label: 'Product',
			href: '/product',
			children: [
				{ label: 'Boards', href: '/product/boards', description: 'Plan work in columns.' },
				{ label: 'Timelines', href: '/product/timelines', description: 'Milestones on one schedule.' }
			]
		},
		{ label: 'Pricing', href: '/pricing' },
		{ label: 'Blog', href: '/blog' }
	];
</script>

<header class="mx-auto flex min-h-16 max-w-7xl items-center gap-10 px-4 sm:px-6">
	<a href="/" class="font-semibold">Example</a>
	<DropdownNav {items} currentPath={page.url.pathname} />
</header>

Dropdown navigation#

A header's top-level row where some items open a short panel of related pages. Use it when a section has five to ten destinations worth naming in the header; when it has three columns of them and a promotion, reach for a mega menu instead.

A group is { label, href?, description?, id?, children }. Its button only opens the panel, so the section's own page, when there is one, becomes the panel's first row: the overview link, labelled by overviewLabel ("Product overview" by default) with the group's description under it. Leave href off a group that has no landing page.

Placing it in a header#

Put the nav beside the brand in a flex row. The row pulls out by its items' 12 px padding, so label text lines up with the edge of whatever sits above or below it. Panels hang 8 px under their trigger, pulled out by their own padding so each row's text starts under the trigger's label; near the end of the viewport they hang from the trigger's end edge instead.

Give the header room for the panel to overlap the page (it is absolutely positioned with z-30), and do not clip it with overflow: hidden on an ancestor.

panelFooter receives the group and renders into a raised shelf across the panel's foot. One line and one link reads best: a price note, a support line, a link to the full index. Render nothing for a group and its shelf disappears.

Small screens#

The row wraps; it does not become a menu button. Below your breakpoint, hide this nav and show a drawer or an accordion of the same items.

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
itemsNavItem[]YesNoneTop-level items: { label, href } links, and { label, href?, description?, id?, children } groups whose children ({ label, href, description? }) open in a panel.
currentPathstringNoNoneCurrent URL pathname, for example page.url.pathname. The matching link gets aria-current="page" and its group button the current-section mark: semibold with a small accent dot.
openOnHoverbooleanNofalseAlso opens a group when a mouse rests on it for 150 ms, and closes it 200 ms after the pointer leaves. Click, touch and keyboard work the same either way.
overviewLabel(groupLabel: string) => stringNo(groupLabel) => `${groupLabel} overview`Text of the overview link, the first row of a group's panel when the group has an href. Translate it with the rest of the nav.
panelFooterSnippet<[NavGroup]>NoNoneContent for the shelf at the foot of each panel, given the group. Render nothing for a group and its shelf is hidden.
currentSectionLabelstringNo'current section'Visually hidden suffix on a group button whose panel holds the current page.
labelstringNo'Main'Accessible name of the nav landmark.

Customization#

On this page

Change content through props and the panelFooter snippet, and recolour through six --dropdown-nav-* CSS variables on any ancestor.

  • Colours: --dropdown-nav-accent is the current dot and focus ring; -ink is labels and hovered text; -muted is resting top-level text and descriptions; -hairline is the panel ring, overview divider and shelf edge; -surface is the panel; -hover is the hover fill. Current, pressed and shelf fills mix from the ink.
  • Dark header: on a zinc-950 band set --dropdown-nav-ink: #fafafa, --dropdown-nav-muted: #a1a1aa, --dropdown-nav-hairline: rgb(255 255 255 / 0.1), --dropdown-nav-surface: #18181b, --dropdown-nav-hover: rgb(255 255 255 / 0.07) and --dropdown-nav-accent: #fafafa on the header element.
  • Cream or tinted page: set --dropdown-nav-surface to a lighter step of the page colour and --dropdown-nav-hover to a darker one, for example #fffdf8 and rgb(120 80 20 / 0.06).
  • Panel width: w-80 (320 px) on the panel in parts/NavGroup.svelte; it is capped at the viewport less 16 px.
  • Overview link: omit a group's href for no overview row, or pass overviewLabel to change its text, for example (label) => `All ${label.toLowerCase()}`.
  • Footer shelf: pass panelFooter and switch on group.id (or group.label); keep it to one line and at most one link.
  • Hover opening: openOnHover={true} adds it for a mouse; the 150 ms and 200 ms delays are in NavGroup.svelte's hover handlers.
  • Density: top-level items and rows are 36 px tall (44 px under a coarse pointer), set by min-h-9 and pointer-coarse:min-h-11.

Public CSS variables

VariableToken
--dropdown-nav-accentaccent
--dropdown-nav-inkink
--dropdown-nav-mutedmuted
--dropdown-nav-hairlinehairline
--dropdown-nav-surfacesurface
--dropdown-nav-hoverhover

Accessibility#

On this page
  • One nav landmark named by label; give it a distinct name if the page has other nav regions.
  • Follows the APG disclosure navigation pattern: each group is a button with aria-expanded and aria-controls, and its panel is a plain list of links. No menu or menuitem roles.
  • A group's own page is never on its button; it is the overview link, the first row of the panel.
  • Enter or Space toggles a panel; Down Arrow on a button opens it and moves into the links; Up and Down move between links (Up from the first returns to the button); Home and End jump to the first or last link. On the top-level row, Left and Right move between items (mirrored right to left) and Home and End jump to the ends. Tab is never trapped.
  • Escape closes the open panel and returns focus to its button; focus leaving the item (tabbing past the last link) or a press outside closes it. Opening one group closes any other.
  • Closed panels carry the hidden and inert attributes, so their links leave the tab order and the accessibility tree the moment they close, even while the panel fades out.
  • The current link gets aria-current="page" and a fill and heavier weight; its group button is set semibold with a 4 px accent dot under its label (a border, so forced colours keep it) and a visually hidden currentSectionLabel suffix; a current top-level link gets the same mark. Labels reserve their semibold width, so the mark never moves the row. Translate that suffix.
  • Chevrons and the overview arrow are aria-hidden. Items show a 2 px focus outline in the accent colour with a 2 px offset and grow to 44 px under a coarse pointer.
  • With openOnHover, a panel opened by the mouse is kept, not closed, by the click that confirms it, and any key pressed inside it keeps it open when the pointer leaves. A pending hover never overrides a click, a key or Escape.
  • IDs come from $props.id(), so two navs on one page keep distinct aria-controls targets.

Known limitations

  • Hover opening is for a mouse only; there is no safe-triangle path prediction, only a bridge over the gap under the trigger and a 200 ms grace period.
  • Panel content in the footer shelf is yours: keep its links accessible and its text inside 4.5:1.

Release details#

On this page
Integration
  • Local interaction
  • Requires client-side JavaScript to be interactive
  • Server-side rendering supported
Dependencies
No additional runtime packages beyond Svelte and Tailwind CSS
License

MIT. Default license approval is pending; see the license status before adopting the source.

Version history
  • 1.0.0 (Published) Current release · 30 September 2026

Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.