Component
Transparent Navbar
A navbar intended to sit over a page header or hero: the surface and bottom border are removed through the variant prop and the semantic token system, with no gradients, no glassmorphism, and no backdrop blur.
- Component
- React
- TSX
- Tailwind CSS
- MIT
What it does
Capabilities
- Semantic nav landmark
- Real anchors
- Aria-current active item
- Mobile disclosure
- Transparent surface
- Overlaid on page header
- No gradients/glassmorphism
- Aria-current='page' active item
- Aria-expanded + aria-controls toggle
- Aria-haspopup dropdown triggers
- Focus restoration on close
- Aria-disabled inactive items
Preview
Live component
9:41 100%
devsnips.site/library/react/components/navbar/navbar-transparent/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add React/Components/Navbar/navbar-transparent React/Components/Navbar/navbar-transparent import {
createContext,
useContext,
useEffect,
useId,
useLayoutEffect,
useRef,
useState,
} from "react";
import type {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
HTMLAttributes,
KeyboardEvent as ReactKeyboardEvent,
ReactNode,
RefObject,
} from "react";
/**
* DevSnips React Navbar — transparent.
*
* Intended for use over a page header or hero: `variant="transparent"`
* removes the surface and bottom border while every link, action, focus
* ring, and panel keeps reading the same semantic tokens — no gradients,
* no glassmorphism, no backdrop blur. Built entirely from the shared
* Navbar primitives; see the `navbar` reference for the full system
* documentation.
*/
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
export type NavbarBreakpoint = "sm" | "md" | "lg";
export type NavbarVariant = "default" | "transparent";
export type NavbarSectionAlign = "start" | "center" | "end";
export type NavbarActionVariant = "primary" | "outline" | "ghost";
export type NavbarMobilePlacement = "panel" | "side";
export type NavbarDropdownPlacement = "bottom-start" | "bottom-end";
/* ------------------------------------------------------------------------ */
/* Shared class constants (single visual system) */
/* ------------------------------------------------------------------------ */
// Navigation height 48–56px (DESIGN_TOKENS §22): the bar is h-14 (56px).
const NAV_CLASSES =
"border-b border-[var(--ds-color-border)] bg-[var(--ds-color-surface)]";
const NAV_TRANSPARENT_CLASSES = "border-b border-transparent bg-transparent";
const BAR_CLASSES = "relative mx-auto flex h-14 max-w-6xl items-center gap-2 px-4 sm:px-6";
const BRAND_CLASSES =
"inline-flex shrink-0 items-center gap-2 rounded-[var(--ds-radius-sm)] text-sm font-semibold leading-5 tracking-tight text-[var(--ds-color-foreground)] transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const SECTION_ALIGN_CLASSES: Record<NavbarSectionAlign, string> = {
start: "flex min-w-0 items-center gap-1",
center: "flex min-w-0 flex-1 items-center justify-center gap-1",
end: "ml-auto flex min-w-0 items-center justify-end gap-2",
};
const LINK_LAYOUT =
"inline-flex items-center gap-1.5 rounded-[var(--ds-radius-sm)] px-2.5 py-1.5 text-sm font-medium leading-5 transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const LINK_MOBILE_LAYOUT =
"flex w-full items-center gap-1.5 rounded-[var(--ds-radius-sm)] px-3 py-2 text-sm font-medium leading-5 transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const LINK_IDLE =
"text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-surface-hover)] hover:text-[var(--ds-color-foreground)]";
const LINK_ACTIVE = "bg-[var(--ds-color-surface-active)] text-[var(--ds-color-foreground)]";
const LINK_DISABLED = "pointer-events-none text-[var(--ds-color-muted-foreground)] opacity-50";
const ACTION_LAYOUT =
"inline-flex h-9 items-center justify-center gap-2 rounded-[var(--ds-radius-sm)] px-3 text-sm font-medium leading-5 transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none";
const ACTION_VARIANT_CLASSES: Record<NavbarActionVariant, string> = {
primary:
"bg-[var(--ds-color-primary)] text-[var(--ds-color-primary-foreground)] shadow-[var(--ds-shadow-xs)] hover:opacity-90",
outline:
"border border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] text-[var(--ds-color-foreground)] shadow-[var(--ds-shadow-xs)] hover:bg-[var(--ds-color-surface-hover)]",
ghost:
"text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-surface-hover)] hover:text-[var(--ds-color-foreground)]",
};
const TOGGLE_CLASSES =
"ml-auto inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--ds-radius-sm)] border border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] text-[var(--ds-color-foreground)] shadow-[var(--ds-shadow-xs)] transition-colors duration-150 ease-out hover:bg-[var(--ds-color-surface-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const MOBILE_PANEL_CLASSES =
"absolute inset-x-0 top-full z-40 max-h-[calc(100dvh-4rem)] overflow-y-auto border-b border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] shadow-[var(--ds-shadow-md)]";
const MOBILE_SIDE_CLASSES =
"fixed inset-y-0 left-0 z-50 flex w-72 max-w-[calc(100vw-4rem)] flex-col border-r border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] shadow-[var(--ds-shadow-lg)]";
const MOBILE_OVERLAY_CLASSES =
"fixed inset-0 z-40 bg-[var(--ds-color-overlay)]";
const MOBILE_CONTENT_CLASSES = "flex flex-col gap-1 px-4 py-4";
const DROPDOWN_PANEL_CLASSES =
"absolute z-40 mt-1.5 max-h-[min(24rem,calc(100dvh-6rem))] min-w-[13rem] max-w-[calc(100vw-1.5rem)] overflow-y-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-color-border)] bg-[var(--ds-color-surface-elevated)] p-1 shadow-[var(--ds-shadow-md)]";
const DROPDOWN_ITEM_LAYOUT =
"flex w-full items-center gap-2 rounded-[var(--ds-radius-sm)] px-2 py-1.5 text-left text-[13px] leading-5 transition-colors duration-150 ease-out focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const DIVIDER_CLASSES = "mx-1 my-1 h-px bg-[var(--ds-color-border)]";
const CONTENT_VISIBLE_CLASSES: Record<NavbarBreakpoint, string> = {
sm: "hidden sm:flex",
md: "hidden md:flex",
lg: "hidden lg:flex",
};
const BELOW_BREAKPOINT_CLASSES: Record<NavbarBreakpoint, string> = {
sm: "sm:hidden",
md: "md:hidden",
lg: "lg:hidden",
};
const DROPDOWN_PLACEMENT_CLASSES: Record<NavbarDropdownPlacement, string> = {
"bottom-start": "left-0 top-full",
"bottom-end": "right-0 top-full",
};
function MenuIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M4 7h16" />
<path d="M4 12h16" />
<path d="M4 17h16" />
</svg>
);
}
function CloseIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
function ChevronDown({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
function ExternalLinkIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M15 3h6v6" />
<path d="M10 14 21 3" />
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
</svg>
);
}
/* ------------------------------------------------------------------------ */
/* Navbar (root) context */
/* ------------------------------------------------------------------------ */
interface NavbarContextValue {
mobileOpen: boolean;
toggleMobile(): void;
closeMobile(): void;
toggleRef: RefObject<HTMLButtonElement>;
mobileRegionId: string;
breakpoint: NavbarBreakpoint;
}
const NavbarContext = createContext<NavbarContextValue | null>(null);
function useNavbar(component: string): NavbarContextValue {
const context = useContext(NavbarContext);
if (!context) {
throw new Error(`<${component}> must be rendered inside <Navbar>.`);
}
return context;
}
/** True when rendered inside `<NavbarMobileContent>` — links stack full-width. */
const NavbarMobileAreaContext = createContext(false);
export interface NavbarProps {
/** Mobile-menu open state (controlled). */
open?: boolean;
/** Initial mobile-menu open state (uncontrolled). */
defaultOpen?: boolean;
/** Called whenever the mobile menu requests to open or close. */
onOpenChange?: (open: boolean) => void;
/** Accessible name of the navigation landmark. */
label?: string;
/** Responsive breakpoint below which the desktop content collapses. */
breakpoint?: NavbarBreakpoint;
/** `transparent` removes the surface + bottom border for use over a page header. */
variant?: NavbarVariant;
className?: string;
children?: ReactNode;
}
export function Navbar({
open,
defaultOpen = false,
onOpenChange,
label = "Main",
breakpoint = "md",
variant = "default",
className,
children,
}: NavbarProps) {
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const isControlled = open !== undefined;
const actualOpen = isControlled ? open : internalOpen;
const rootRef = useRef<HTMLElement>(null);
const toggleRef = useRef<HTMLButtonElement>(null);
const reactId = useId();
const mobileRegionId = `ds-navbar-mobile${reactId}`;
function requestOpen(next: boolean) {
if (!isControlled) setInternalOpen(next);
onOpenChange?.(next);
}
const context: NavbarContextValue = {
mobileOpen: actualOpen,
toggleMobile() {
requestOpen(!actualOpen);
},
closeMobile() {
if (actualOpen) requestOpen(false);
},
toggleRef,
mobileRegionId,
breakpoint,
};
// Escape closes the mobile navigation from anywhere in the document.
useEffect(() => {
if (!actualOpen) return;
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
event.preventDefault();
requestOpen(false);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actualOpen]);
// A pointer down outside the navbar closes an open mobile navigation.
useEffect(() => {
if (!actualOpen) return;
function onPointerDown(event: PointerEvent) {
const root = rootRef.current;
if (root && !root.contains(event.target as Node)) requestOpen(false);
}
document.addEventListener("pointerdown", onPointerDown, true);
return () => document.removeEventListener("pointerdown", onPointerDown, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actualOpen]);
// Focus restoration: when the mobile region unmounts, focus would be
// stranded on <body> — return it to the toggle. Closing via the toggle
// itself keeps focus on the toggle (no-op), and Tab moves on naturally.
const wasOpenRef = useRef(false);
useEffect(() => {
if (wasOpenRef.current && !actualOpen) {
const active = document.activeElement;
const toggle = toggleRef.current;
if (toggle && toggle.isConnected && (active === null || active === document.body)) {
toggle.focus();
}
}
wasOpenRef.current = actualOpen;
}, [actualOpen]);
return (
<NavbarContext.Provider value={context}>
<nav
ref={rootRef}
aria-label={label}
className={cx(variant === "transparent" ? NAV_TRANSPARENT_CLASSES : NAV_CLASSES, className)}
>
<div className={BAR_CLASSES}>{children}</div>
</nav>
</NavbarContext.Provider>
);
}
/* ------------------------------------------------------------------------ */
/* Brand / content regions */
/* ------------------------------------------------------------------------ */
export interface NavbarBrandProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
/** Home URL the brand points at. */
href?: string;
children?: ReactNode;
}
export function NavbarBrand({ href = "/", className, children, ...rest }: NavbarBrandProps) {
return (
<a href={href} className={cx(BRAND_CLASSES, className)} {...rest}>
{children}
</a>
);
}
export interface NavbarContentProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
}
export function NavbarContent({ className, children, ...rest }: NavbarContentProps) {
const context = useNavbar("NavbarContent");
return (
<div
className={cx("min-w-0 flex-1 items-center gap-4", CONTENT_VISIBLE_CLASSES[context.breakpoint], className)}
{...rest}
>
{children}
</div>
);
}
export interface NavbarSectionProps extends HTMLAttributes<HTMLUListElement> {
/** Region of the bar: `start` (after the brand), `center`, or `end` (trailing). */
align?: NavbarSectionAlign;
children?: ReactNode;
}
export function NavbarSection({ align = "start", className, children, ...rest }: NavbarSectionProps) {
return (
<ul role="list" className={cx(SECTION_ALIGN_CLASSES[align], className)} {...rest}>
{children}
</ul>
);
}
export interface NavbarItemProps extends HTMLAttributes<HTMLLIElement> {
children?: ReactNode;
}
export function NavbarItem({ className, children, ...rest }: NavbarItemProps) {
return (
<li className={cx("flex min-w-0", className)} {...rest}>
{children}
</li>
);
}
/* ------------------------------------------------------------------------ */
/* Links and actions */
/* ------------------------------------------------------------------------ */
export interface NavbarLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
href?: string;
/** Marks the current page: `aria-current="page"` + the active surface. */
active?: boolean;
/** Opens in a new tab (`target="_blank" rel="noreferrer"`) with an indicator. */
external?: boolean;
/** Renders a non-interactive `aria-disabled` span — never a dead anchor. */
disabled?: boolean;
children?: ReactNode;
}
export function NavbarLink({
href = "#",
active = false,
external = false,
disabled = false,
onClick,
className,
children,
...rest
}: NavbarLinkProps) {
const context = useNavbar("NavbarLink");
const inMobileArea = useContext(NavbarMobileAreaContext);
const classes = cx(
inMobileArea ? LINK_MOBILE_LAYOUT : LINK_LAYOUT,
disabled ? LINK_DISABLED : active ? LINK_ACTIVE : LINK_IDLE,
className,
);
if (disabled) {
return (
<span aria-disabled="true" className={classes}>
{children}
</span>
);
}
return (
<a
href={href}
aria-current={active ? "page" : undefined}
target={external ? "_blank" : undefined}
rel={external ? "noreferrer" : undefined}
onClick={(event) => {
onClick?.(event);
context.closeMobile();
}}
className={classes}
{...rest}
>
<span className="min-w-0 truncate">{children}</span>
{external ? (
<>
<ExternalLinkIcon className="size-3.5 shrink-0 text-[var(--ds-color-muted-foreground)]" />
<span className="sr-only">(opens in a new tab)</span>
</>
) : null}
</a>
);
}
interface NavbarActionBaseProps {
/** Visual weight of the action. */
variant?: NavbarActionVariant;
className?: string;
children?: ReactNode;
}
export type NavbarActionProps =
| (NavbarActionBaseProps & { href: string } & Omit<
AnchorHTMLAttributes<HTMLAnchorElement>,
"href"
>)
| (NavbarActionBaseProps & { href?: undefined } & ButtonHTMLAttributes<HTMLButtonElement>);
export function NavbarAction(props: NavbarActionProps) {
if (props.href !== undefined) {
const { variant = "primary", href, className, children, ...rest } = props;
return (
<a href={href} className={cx(ACTION_LAYOUT, ACTION_VARIANT_CLASSES[variant], className)} {...rest}>
{children}
</a>
);
}
const { variant = "primary", className, children, type = "button", ...rest } = props;
return (
<button type={type} className={cx(ACTION_LAYOUT, ACTION_VARIANT_CLASSES[variant], className)} {...rest}>
{children}
</button>
);
}
/* ------------------------------------------------------------------------ */
/* Mobile navigation */
/* ------------------------------------------------------------------------ */
export interface NavbarToggleProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Accessible name override (defaults to "Open/Close navigation menu"). */
label?: string;
}
export function NavbarToggle({ label, className, onClick, ...rest }: NavbarToggleProps) {
const context = useNavbar("NavbarToggle");
// The first mounted toggle (the bar's) permanently owns the shared ref, so
// a second toggle (e.g. a close button inside a side panel) never leaves
// the focus-restore target pointing at an unmounted element.
function claimRef(node: HTMLButtonElement | null) {
if (node && !context.toggleRef.current) {
(context.toggleRef as { current: HTMLButtonElement | null }).current = node;
}
}
return (
<button
ref={claimRef}
type="button"
aria-expanded={context.mobileOpen}
aria-controls={context.mobileRegionId}
aria-label={label ?? (context.mobileOpen ? "Close navigation menu" : "Open navigation menu")}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented) return;
context.toggleMobile();
}}
className={cx(TOGGLE_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint], className)}
{...rest}
>
{context.mobileOpen ? <CloseIcon className="size-5" /> : <MenuIcon className="size-5" />}
</button>
);
}
export interface NavbarMobileProps {
/**
* `panel` (default): a full-width region disclosed directly under the bar.
* `side`: a compact side panel with an overlay — Escape and overlay
* interaction close it, body scroll is locked while open, and focus moves
* into the panel (focus is NOT trapped: this is a navigation disclosure,
* not a modal dialog).
*/
placement?: NavbarMobilePlacement;
className?: string;
children?: ReactNode;
}
export function NavbarMobile({ placement = "panel", className, children }: NavbarMobileProps) {
const context = useNavbar("NavbarMobile");
const panelRef = useRef<HTMLDivElement>(null);
const open = context.mobileOpen;
// Side panel only: move focus into the panel on open and lock body scroll
// (with scrollbar-width compensation so the page does not shift).
useEffect(() => {
if (!open || placement !== "side") return;
const panel = panelRef.current;
const first = panel?.querySelector<HTMLElement>("a[href], button:not([disabled])");
first?.focus();
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
const previousOverflow = document.body.style.overflow;
const previousPaddingRight = document.body.style.paddingRight;
document.body.style.overflow = "hidden";
if (scrollbarWidth > 0) document.body.style.paddingRight = `${scrollbarWidth}px`;
return () => {
document.body.style.overflow = previousOverflow;
document.body.style.paddingRight = previousPaddingRight;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, placement]);
if (!open) return null;
if (placement === "side") {
return (
<>
<div
aria-hidden="true"
data-ds-navbar-overlay=""
className={cx(MOBILE_OVERLAY_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint])}
onPointerDown={() => context.closeMobile()}
/>
<div
ref={panelRef}
id={context.mobileRegionId}
className={cx(MOBILE_SIDE_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint], className)}
>
{children}
</div>
</>
);
}
return (
<div
ref={panelRef}
id={context.mobileRegionId}
className={cx(MOBILE_PANEL_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint], className)}
>
{children}
</div>
);
}
export interface NavbarMobileContentProps extends HTMLAttributes<HTMLUListElement> {
children?: ReactNode;
}
export function NavbarMobileContent({ className, children, ...rest }: NavbarMobileContentProps) {
return (
<NavbarMobileAreaContext.Provider value={true}>
<ul role="list" className={cx(MOBILE_CONTENT_CLASSES, className)} {...rest}>
{children}
</ul>
</NavbarMobileAreaContext.Provider>
);
}
/* ------------------------------------------------------------------------ */
/* Navigation dropdown (disclosure pattern — links stay real anchors) */
/* ------------------------------------------------------------------------ */
interface NavbarDropdownContextValue {
open: boolean;
initialFocus: "first" | "last";
requestOpen(next: boolean, focusTarget?: "first" | "last"): void;
closeMenu(options?: { refocus?: boolean }): void;
triggerRef: RefObject<HTMLButtonElement>;
rootRef: RefObject<HTMLDivElement>;
triggerId: string;
contentId: string;
placement: NavbarDropdownPlacement;
}
const NavbarDropdownContext = createContext<NavbarDropdownContextValue | null>(null);
function useNavbarDropdown(component: string): NavbarDropdownContextValue {
const context = useContext(NavbarDropdownContext);
if (!context) {
throw new Error(`<${component}> must be rendered inside <NavbarDropdown>.`);
}
return context;
}
/** Focusable dropdown items owned directly by `content`. */
function dropdownItems(content: HTMLElement): HTMLElement[] {
return Array.from(
content.querySelectorAll<HTMLElement>("a[href], button"),
).filter(
(el) =>
el.closest("[data-ds-navbar-dropdown-content]") === content &&
!el.hasAttribute("disabled") &&
el.getAttribute("aria-disabled") !== "true",
);
}
function focusDropdownItem(items: HTMLElement[], index: number): void {
if (items.length === 0) return;
const wrapped = ((index % items.length) + items.length) % items.length;
items[wrapped].focus();
}
export interface NavbarDropdownProps {
/** Initial open state (uncontrolled). */
defaultOpen?: boolean;
/** Alignment of the panel relative to the trigger; flips to stay in the viewport. */
placement?: NavbarDropdownPlacement;
className?: string;
children?: ReactNode;
}
export function NavbarDropdown({
defaultOpen = false,
placement = "bottom-start",
className,
children,
}: NavbarDropdownProps) {
const [open, setOpen] = useState(defaultOpen);
const [initialFocus, setInitialFocus] = useState<"first" | "last">("first");
const triggerRef = useRef<HTMLButtonElement>(null);
const rootRef = useRef<HTMLDivElement>(null);
const reactId = useId();
const triggerId = `ds-navbar-dd-trigger${reactId}`;
const contentId = `ds-navbar-dd-content${reactId}`;
function requestOpen(next: boolean, focusTarget: "first" | "last" = "first") {
setInitialFocus(focusTarget);
setOpen(next);
}
function closeMenu(options?: { refocus?: boolean }) {
const refocus = options?.refocus ?? true;
setOpen(false);
if (refocus) triggerRef.current?.focus();
}
// A pointer down outside the dropdown closes it without stealing focus.
useEffect(() => {
if (!open) return;
function onPointerDown(event: PointerEvent) {
const root = rootRef.current;
if (root && !root.contains(event.target as Node)) closeMenu({ refocus: false });
}
document.addEventListener("pointerdown", onPointerDown, true);
return () => document.removeEventListener("pointerdown", onPointerDown, true);
}, [open]);
const context: NavbarDropdownContextValue = {
open,
initialFocus,
requestOpen,
closeMenu,
triggerRef,
rootRef,
triggerId,
contentId,
placement,
};
return (
<NavbarDropdownContext.Provider value={context}>
<div ref={rootRef} className={cx("relative flex min-w-0", className)}>
{children}
</div>
</NavbarDropdownContext.Provider>
);
}
export interface NavbarDropdownTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** Meaningful leading icon (rendered aria-hidden). */
icon?: ReactNode;
children?: ReactNode;
}
export function NavbarDropdownTrigger({
icon,
className,
children,
onClick,
onKeyDown,
...rest
}: NavbarDropdownTriggerProps) {
const context = useNavbarDropdown("NavbarDropdownTrigger");
function handleKeyDown(event: ReactKeyboardEvent<HTMLButtonElement>) {
onKeyDown?.(event);
if (event.defaultPrevented) return;
if (event.key === "ArrowDown") {
event.preventDefault();
context.requestOpen(true, "first");
} else if (event.key === "ArrowUp") {
event.preventDefault();
context.requestOpen(true, "last");
}
}
return (
<button
ref={context.triggerRef}
type="button"
id={context.triggerId}
aria-haspopup="true"
aria-expanded={context.open}
aria-controls={context.contentId}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented) return;
context.requestOpen(!context.open);
}}
onKeyDown={handleKeyDown}
className={cx(
LINK_LAYOUT,
LINK_IDLE,
"aria-expanded:bg-[var(--ds-color-surface-hover)] aria-expanded:text-[var(--ds-color-foreground)]",
className,
)}
{...rest}
>
{icon ? (
<span aria-hidden="true" className="inline-flex shrink-0 [&_svg]:size-3.5">
{icon}
</span>
) : null}
<span className="min-w-0 truncate">{children}</span>
<ChevronDown
className={cx(
"size-3.5 shrink-0 text-[var(--ds-color-muted-foreground)] transition-transform duration-150 ease-out motion-reduce:transition-none",
context.open && "rotate-180",
)}
/>
</button>
);
}
export interface NavbarDropdownContentProps extends HTMLAttributes<HTMLDivElement> {
/** Explicit accessible name; otherwise the panel is labelled by its trigger. */
"aria-label"?: string;
children?: ReactNode;
}
export function NavbarDropdownContent({
onKeyDown,
className,
children,
...rest
}: NavbarDropdownContentProps) {
const context = useNavbarDropdown("NavbarDropdownContent");
const contentRef = useRef<HTMLDivElement>(null);
const [resolved, setResolved] = useState<NavbarDropdownPlacement>(context.placement);
const [measured, setMeasured] = useState(false);
const open = context.open;
// Move focus into the panel when it opens (first item, or last when the
// trigger was invoked with ArrowUp).
useEffect(() => {
if (!open) return;
const node = contentRef.current;
if (!node) return;
const items = dropdownItems(node);
focusDropdownItem(items, context.initialFocus === "last" ? items.length - 1 : 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
// Measure against the viewport and flip the horizontal alignment when the
// preferred side would overflow. Runs before paint; the panel stays
// `invisible` until the first measurement so the flip never flashes.
useLayoutEffect(() => {
if (!open) {
setMeasured(false);
setResolved(context.placement);
return;
}
const node = contentRef.current;
const trigger = context.triggerRef.current;
if (!node || !trigger) return;
const t = trigger.getBoundingClientRect();
const c = node.getBoundingClientRect();
const margin = 8;
let align: "start" | "end" = context.placement === "bottom-end" ? "end" : "start";
if (align === "start" && t.left + c.width > window.innerWidth - margin && t.right - c.width >= margin) {
align = "end";
} else if (align === "end" && t.right - c.width < margin && t.left + c.width <= window.innerWidth - margin) {
align = "start";
}
setResolved(align === "end" ? "bottom-end" : "bottom-start");
setMeasured(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, context.placement]);
function handleKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {
onKeyDown?.(event);
if (event.defaultPrevented) return;
const node = contentRef.current;
if (!node) return;
const items = dropdownItems(node);
const current = items.indexOf(document.activeElement as HTMLElement);
switch (event.key) {
case "ArrowDown":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, current + 1);
break;
case "ArrowUp":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, current - 1);
break;
case "Home":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, 0);
break;
case "End":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, items.length - 1);
break;
case "Escape":
event.preventDefault();
event.stopPropagation();
context.closeMenu();
break;
case "Tab":
// Let focus leave naturally; close without stealing it back.
context.closeMenu({ refocus: false });
break;
default:
break;
}
}
if (!open) return null;
return (
<div
ref={contentRef}
id={context.contentId}
data-ds-navbar-dropdown-content=""
aria-labelledby={rest["aria-label"] ? undefined : context.triggerId}
onKeyDown={handleKeyDown}
className={cx(
DROPDOWN_PANEL_CLASSES,
DROPDOWN_PLACEMENT_CLASSES[resolved],
!measured && "invisible",
className,
)}
{...rest}
>
{children}
</div>
);
}
export interface NavbarDropdownItemProps {
/** Navigation target. When omitted, the item renders a `<button>` action. */
href?: string;
/** Marks the current page: `aria-current="page"` + the active surface. */
active?: boolean;
/** Opens in a new tab (`target="_blank" rel="noreferrer"`) with an indicator. */
external?: boolean;
/** Renders a non-interactive `aria-disabled` span — skipped by arrow keys. */
disabled?: boolean;
/** Meaningful leading icon (rendered aria-hidden). */
icon?: ReactNode;
/** Called when the item is activated, before the dropdown closes. */
onSelect?: () => void;
/** Accessible name override (e.g. icon-forward items). */
"aria-label"?: string;
className?: string;
children?: ReactNode;
}
export function NavbarDropdownItem({
href,
active = false,
external = false,
disabled = false,
icon,
onSelect,
className,
children,
...rest
}: NavbarDropdownItemProps) {
const context = useNavbarDropdown("NavbarDropdownItem");
const classes = cx(
DROPDOWN_ITEM_LAYOUT,
disabled ? LINK_DISABLED : active ? LINK_ACTIVE : LINK_IDLE,
className,
);
const iconSlot = icon ? (
<span
aria-hidden="true"
className={cx(
"inline-flex shrink-0 [&_svg]:size-4",
active ? "text-current" : "text-[var(--ds-color-muted-foreground)]",
)}
>
{icon}
</span>
) : null;
const label = <span className="min-w-0 flex-1 truncate">{children}</span>;
const externalMarker = external ? (
<>
<ExternalLinkIcon className="size-3.5 shrink-0 text-[var(--ds-color-muted-foreground)]" />
<span className="sr-only">(opens in a new tab)</span>
</>
) : null;
if (disabled) {
return (
<span aria-disabled="true" className={classes}>
{iconSlot}
{label}
</span>
);
}
if (href !== undefined) {
return (
<a
href={href}
aria-current={active ? "page" : undefined}
target={external ? "_blank" : undefined}
rel={external ? "noreferrer" : undefined}
onClick={() => {
onSelect?.();
context.closeMenu();
}}
className={classes}
{...rest}
>
{iconSlot}
{label}
{externalMarker}
</a>
);
}
return (
<button
type="button"
onClick={() => {
onSelect?.();
context.closeMenu();
}}
className={classes}
{...rest}
>
{iconSlot}
{label}
</button>
);
}
export interface NavbarDividerProps extends HTMLAttributes<HTMLDivElement> {
children?: undefined;
}
export function NavbarDivider({ className, ...rest }: NavbarDividerProps) {
return (
<div
role="separator"
aria-orientation="horizontal"
className={cx(DIVIDER_CLASSES, className)}
{...rest}
/>
);
} /* DevSnips React — JavaScript parity build.
* Same API, behavior, and classes as code.tsx; TypeScript types removed.
* Regenerated from code.tsx — edit code.tsx and re-run the generator.
*/
import {
createContext,
useContext,
useEffect,
useId,
useLayoutEffect,
useRef,
useState
} from "react";
function cx(...parts) {
return parts.filter(Boolean).join(" ");
}
const NAV_CLASSES = "border-b border-[var(--ds-color-border)] bg-[var(--ds-color-surface)]";
const NAV_TRANSPARENT_CLASSES = "border-b border-transparent bg-transparent";
const BAR_CLASSES = "relative mx-auto flex h-14 max-w-6xl items-center gap-2 px-4 sm:px-6";
const BRAND_CLASSES = "inline-flex shrink-0 items-center gap-2 rounded-[var(--ds-radius-sm)] text-sm font-semibold leading-5 tracking-tight text-[var(--ds-color-foreground)] transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const SECTION_ALIGN_CLASSES = {
start: "flex min-w-0 items-center gap-1",
center: "flex min-w-0 flex-1 items-center justify-center gap-1",
end: "ml-auto flex min-w-0 items-center justify-end gap-2"
};
const LINK_LAYOUT = "inline-flex items-center gap-1.5 rounded-[var(--ds-radius-sm)] px-2.5 py-1.5 text-sm font-medium leading-5 transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const LINK_MOBILE_LAYOUT = "flex w-full items-center gap-1.5 rounded-[var(--ds-radius-sm)] px-3 py-2 text-sm font-medium leading-5 transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const LINK_IDLE = "text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-surface-hover)] hover:text-[var(--ds-color-foreground)]";
const LINK_ACTIVE = "bg-[var(--ds-color-surface-active)] text-[var(--ds-color-foreground)]";
const LINK_DISABLED = "pointer-events-none text-[var(--ds-color-muted-foreground)] opacity-50";
const ACTION_LAYOUT = "inline-flex h-9 items-center justify-center gap-2 rounded-[var(--ds-radius-sm)] px-3 text-sm font-medium leading-5 transition-colors duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none";
const ACTION_VARIANT_CLASSES = {
primary: "bg-[var(--ds-color-primary)] text-[var(--ds-color-primary-foreground)] shadow-[var(--ds-shadow-xs)] hover:opacity-90",
outline: "border border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] text-[var(--ds-color-foreground)] shadow-[var(--ds-shadow-xs)] hover:bg-[var(--ds-color-surface-hover)]",
ghost: "text-[var(--ds-color-muted-foreground)] hover:bg-[var(--ds-color-surface-hover)] hover:text-[var(--ds-color-foreground)]"
};
const TOGGLE_CLASSES = "ml-auto inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--ds-radius-sm)] border border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] text-[var(--ds-color-foreground)] shadow-[var(--ds-shadow-xs)] transition-colors duration-150 ease-out hover:bg-[var(--ds-color-surface-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const MOBILE_PANEL_CLASSES = "absolute inset-x-0 top-full z-40 max-h-[calc(100dvh-4rem)] overflow-y-auto border-b border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] shadow-[var(--ds-shadow-md)]";
const MOBILE_SIDE_CLASSES = "fixed inset-y-0 left-0 z-50 flex w-72 max-w-[calc(100vw-4rem)] flex-col border-r border-[var(--ds-color-border)] bg-[var(--ds-color-surface)] shadow-[var(--ds-shadow-lg)]";
const MOBILE_OVERLAY_CLASSES = "fixed inset-0 z-40 bg-[var(--ds-color-overlay)]";
const MOBILE_CONTENT_CLASSES = "flex flex-col gap-1 px-4 py-4";
const DROPDOWN_PANEL_CLASSES = "absolute z-40 mt-1.5 max-h-[min(24rem,calc(100dvh-6rem))] min-w-[13rem] max-w-[calc(100vw-1.5rem)] overflow-y-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-color-border)] bg-[var(--ds-color-surface-elevated)] p-1 shadow-[var(--ds-shadow-md)]";
const DROPDOWN_ITEM_LAYOUT = "flex w-full items-center gap-2 rounded-[var(--ds-radius-sm)] px-2 py-1.5 text-left text-[13px] leading-5 transition-colors duration-150 ease-out focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[var(--ds-color-focus-ring)] motion-reduce:transition-none";
const DIVIDER_CLASSES = "mx-1 my-1 h-px bg-[var(--ds-color-border)]";
const CONTENT_VISIBLE_CLASSES = {
sm: "hidden sm:flex",
md: "hidden md:flex",
lg: "hidden lg:flex"
};
const BELOW_BREAKPOINT_CLASSES = {
sm: "sm:hidden",
md: "md:hidden",
lg: "lg:hidden"
};
const DROPDOWN_PLACEMENT_CLASSES = {
"bottom-start": "left-0 top-full",
"bottom-end": "right-0 top-full"
};
function MenuIcon({ className }) {
return <svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M4 7h16" />
<path d="M4 12h16" />
<path d="M4 17h16" />
</svg>;
}
function CloseIcon({ className }) {
return <svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>;
}
function ChevronDown({ className }) {
return <svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="m6 9 6 6 6-6" />
</svg>;
}
function ExternalLinkIcon({ className }) {
return <svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
focusable="false"
>
<path d="M15 3h6v6" />
<path d="M10 14 21 3" />
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
</svg>;
}
const NavbarContext = createContext(null);
function useNavbar(component) {
const context = useContext(NavbarContext);
if (!context) {
throw new Error(`<${component}> must be rendered inside <Navbar>.`);
}
return context;
}
const NavbarMobileAreaContext = createContext(false);
function Navbar({
open,
defaultOpen = false,
onOpenChange,
label = "Main",
breakpoint = "md",
variant = "default",
className,
children
}) {
const [internalOpen, setInternalOpen] = useState(defaultOpen);
const isControlled = open !== undefined;
const actualOpen = isControlled ? open : internalOpen;
const rootRef = useRef(null);
const toggleRef = useRef(null);
const reactId = useId();
const mobileRegionId = `ds-navbar-mobile${reactId}`;
function requestOpen(next) {
if (!isControlled) setInternalOpen(next);
onOpenChange?.(next);
}
const context = {
mobileOpen: actualOpen,
toggleMobile() {
requestOpen(!actualOpen);
},
closeMobile() {
if (actualOpen) requestOpen(false);
},
toggleRef,
mobileRegionId,
breakpoint
};
useEffect(() => {
if (!actualOpen) return;
function onKeyDown(event) {
if (event.key === "Escape") {
event.preventDefault();
requestOpen(false);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [actualOpen]);
useEffect(() => {
if (!actualOpen) return;
function onPointerDown(event) {
const root = rootRef.current;
if (root && !root.contains(event.target)) requestOpen(false);
}
document.addEventListener("pointerdown", onPointerDown, true);
return () => document.removeEventListener("pointerdown", onPointerDown, true);
}, [actualOpen]);
const wasOpenRef = useRef(false);
useEffect(() => {
if (wasOpenRef.current && !actualOpen) {
const active = document.activeElement;
const toggle = toggleRef.current;
if (toggle && toggle.isConnected && (active === null || active === document.body)) {
toggle.focus();
}
}
wasOpenRef.current = actualOpen;
}, [actualOpen]);
return <NavbarContext.Provider value={context}>
<nav
ref={rootRef}
aria-label={label}
className={cx(variant === "transparent" ? NAV_TRANSPARENT_CLASSES : NAV_CLASSES, className)}
>
<div className={BAR_CLASSES}>{children}</div>
</nav>
</NavbarContext.Provider>;
}
function NavbarBrand({ href = "/", className, children, ...rest }) {
return <a href={href} className={cx(BRAND_CLASSES, className)} {...rest}>
{children}
</a>;
}
function NavbarContent({ className, children, ...rest }) {
const context = useNavbar("NavbarContent");
return <div
className={cx("min-w-0 flex-1 items-center gap-4", CONTENT_VISIBLE_CLASSES[context.breakpoint], className)}
{...rest}
>
{children}
</div>;
}
function NavbarSection({ align = "start", className, children, ...rest }) {
return <ul role="list" className={cx(SECTION_ALIGN_CLASSES[align], className)} {...rest}>
{children}
</ul>;
}
function NavbarItem({ className, children, ...rest }) {
return <li className={cx("flex min-w-0", className)} {...rest}>
{children}
</li>;
}
function NavbarLink({
href = "#",
active = false,
external = false,
disabled = false,
onClick,
className,
children,
...rest
}) {
const context = useNavbar("NavbarLink");
const inMobileArea = useContext(NavbarMobileAreaContext);
const classes = cx(
inMobileArea ? LINK_MOBILE_LAYOUT : LINK_LAYOUT,
disabled ? LINK_DISABLED : active ? LINK_ACTIVE : LINK_IDLE,
className
);
if (disabled) {
return <span aria-disabled="true" className={classes}>
{children}
</span>;
}
return <a
href={href}
aria-current={active ? "page" : undefined}
target={external ? "_blank" : undefined}
rel={external ? "noreferrer" : undefined}
onClick={(event) => {
onClick?.(event);
context.closeMobile();
}}
className={classes}
{...rest}
>
<span className="min-w-0 truncate">{children}</span>
{external ? <>
<ExternalLinkIcon className="size-3.5 shrink-0 text-[var(--ds-color-muted-foreground)]" />
<span className="sr-only">(opens in a new tab)</span>
</> : null}
</a>;
}
function NavbarAction(props) {
if (props.href !== undefined) {
const { variant: variant2 = "primary", href, className: className2, children: children2, ...rest2 } = props;
return <a href={href} className={cx(ACTION_LAYOUT, ACTION_VARIANT_CLASSES[variant2], className2)} {...rest2}>
{children2}
</a>;
}
const { variant = "primary", className, children, type = "button", ...rest } = props;
return <button type={type} className={cx(ACTION_LAYOUT, ACTION_VARIANT_CLASSES[variant], className)} {...rest}>
{children}
</button>;
}
function NavbarToggle({ label, className, onClick, ...rest }) {
const context = useNavbar("NavbarToggle");
function claimRef(node) {
if (node && !context.toggleRef.current) {
context.toggleRef.current = node;
}
}
return <button
ref={claimRef}
type="button"
aria-expanded={context.mobileOpen}
aria-controls={context.mobileRegionId}
aria-label={label ?? (context.mobileOpen ? "Close navigation menu" : "Open navigation menu")}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented) return;
context.toggleMobile();
}}
className={cx(TOGGLE_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint], className)}
{...rest}
>
{context.mobileOpen ? <CloseIcon className="size-5" /> : <MenuIcon className="size-5" />}
</button>;
}
function NavbarMobile({ placement = "panel", className, children }) {
const context = useNavbar("NavbarMobile");
const panelRef = useRef(null);
const open = context.mobileOpen;
useEffect(() => {
if (!open || placement !== "side") return;
const panel = panelRef.current;
const first = panel?.querySelector("a[href], button:not([disabled])");
first?.focus();
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
const previousOverflow = document.body.style.overflow;
const previousPaddingRight = document.body.style.paddingRight;
document.body.style.overflow = "hidden";
if (scrollbarWidth > 0) document.body.style.paddingRight = `${scrollbarWidth}px`;
return () => {
document.body.style.overflow = previousOverflow;
document.body.style.paddingRight = previousPaddingRight;
};
}, [open, placement]);
if (!open) return null;
if (placement === "side") {
return <>
<div
aria-hidden="true"
data-ds-navbar-overlay=""
className={cx(MOBILE_OVERLAY_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint])}
onPointerDown={() => context.closeMobile()}
/>
<div
ref={panelRef}
id={context.mobileRegionId}
className={cx(MOBILE_SIDE_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint], className)}
>
{children}
</div>
</>;
}
return <div
ref={panelRef}
id={context.mobileRegionId}
className={cx(MOBILE_PANEL_CLASSES, BELOW_BREAKPOINT_CLASSES[context.breakpoint], className)}
>
{children}
</div>;
}
function NavbarMobileContent({ className, children, ...rest }) {
return <NavbarMobileAreaContext.Provider value={true}>
<ul role="list" className={cx(MOBILE_CONTENT_CLASSES, className)} {...rest}>
{children}
</ul>
</NavbarMobileAreaContext.Provider>;
}
const NavbarDropdownContext = createContext(null);
function useNavbarDropdown(component) {
const context = useContext(NavbarDropdownContext);
if (!context) {
throw new Error(`<${component}> must be rendered inside <NavbarDropdown>.`);
}
return context;
}
function dropdownItems(content) {
return Array.from(
content.querySelectorAll("a[href], button")
).filter(
(el) => el.closest("[data-ds-navbar-dropdown-content]") === content && !el.hasAttribute("disabled") && el.getAttribute("aria-disabled") !== "true"
);
}
function focusDropdownItem(items, index) {
if (items.length === 0) return;
const wrapped = (index % items.length + items.length) % items.length;
items[wrapped].focus();
}
function NavbarDropdown({
defaultOpen = false,
placement = "bottom-start",
className,
children
}) {
const [open, setOpen] = useState(defaultOpen);
const [initialFocus, setInitialFocus] = useState("first");
const triggerRef = useRef(null);
const rootRef = useRef(null);
const reactId = useId();
const triggerId = `ds-navbar-dd-trigger${reactId}`;
const contentId = `ds-navbar-dd-content${reactId}`;
function requestOpen(next, focusTarget = "first") {
setInitialFocus(focusTarget);
setOpen(next);
}
function closeMenu(options) {
const refocus = options?.refocus ?? true;
setOpen(false);
if (refocus) triggerRef.current?.focus();
}
useEffect(() => {
if (!open) return;
function onPointerDown(event) {
const root = rootRef.current;
if (root && !root.contains(event.target)) closeMenu({ refocus: false });
}
document.addEventListener("pointerdown", onPointerDown, true);
return () => document.removeEventListener("pointerdown", onPointerDown, true);
}, [open]);
const context = {
open,
initialFocus,
requestOpen,
closeMenu,
triggerRef,
rootRef,
triggerId,
contentId,
placement
};
return <NavbarDropdownContext.Provider value={context}>
<div ref={rootRef} className={cx("relative flex min-w-0", className)}>
{children}
</div>
</NavbarDropdownContext.Provider>;
}
function NavbarDropdownTrigger({
icon,
className,
children,
onClick,
onKeyDown,
...rest
}) {
const context = useNavbarDropdown("NavbarDropdownTrigger");
function handleKeyDown(event) {
onKeyDown?.(event);
if (event.defaultPrevented) return;
if (event.key === "ArrowDown") {
event.preventDefault();
context.requestOpen(true, "first");
} else if (event.key === "ArrowUp") {
event.preventDefault();
context.requestOpen(true, "last");
}
}
return <button
ref={context.triggerRef}
type="button"
id={context.triggerId}
aria-haspopup="true"
aria-expanded={context.open}
aria-controls={context.contentId}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented) return;
context.requestOpen(!context.open);
}}
onKeyDown={handleKeyDown}
className={cx(
LINK_LAYOUT,
LINK_IDLE,
"aria-expanded:bg-[var(--ds-color-surface-hover)] aria-expanded:text-[var(--ds-color-foreground)]",
className
)}
{...rest}
>
{icon ? <span aria-hidden="true" className="inline-flex shrink-0 [&_svg]:size-3.5">
{icon}
</span> : null}
<span className="min-w-0 truncate">{children}</span>
<ChevronDown
className={cx(
"size-3.5 shrink-0 text-[var(--ds-color-muted-foreground)] transition-transform duration-150 ease-out motion-reduce:transition-none",
context.open && "rotate-180"
)}
/>
</button>;
}
function NavbarDropdownContent({
onKeyDown,
className,
children,
...rest
}) {
const context = useNavbarDropdown("NavbarDropdownContent");
const contentRef = useRef(null);
const [resolved, setResolved] = useState(context.placement);
const [measured, setMeasured] = useState(false);
const open = context.open;
useEffect(() => {
if (!open) return;
const node = contentRef.current;
if (!node) return;
const items = dropdownItems(node);
focusDropdownItem(items, context.initialFocus === "last" ? items.length - 1 : 0);
}, [open]);
useLayoutEffect(() => {
if (!open) {
setMeasured(false);
setResolved(context.placement);
return;
}
const node = contentRef.current;
const trigger = context.triggerRef.current;
if (!node || !trigger) return;
const t = trigger.getBoundingClientRect();
const c = node.getBoundingClientRect();
const margin = 8;
let align = context.placement === "bottom-end" ? "end" : "start";
if (align === "start" && t.left + c.width > window.innerWidth - margin && t.right - c.width >= margin) {
align = "end";
} else if (align === "end" && t.right - c.width < margin && t.left + c.width <= window.innerWidth - margin) {
align = "start";
}
setResolved(align === "end" ? "bottom-end" : "bottom-start");
setMeasured(true);
}, [open, context.placement]);
function handleKeyDown(event) {
onKeyDown?.(event);
if (event.defaultPrevented) return;
const node = contentRef.current;
if (!node) return;
const items = dropdownItems(node);
const current = items.indexOf(document.activeElement);
switch (event.key) {
case "ArrowDown":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, current + 1);
break;
case "ArrowUp":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, current - 1);
break;
case "Home":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, 0);
break;
case "End":
event.preventDefault();
event.stopPropagation();
focusDropdownItem(items, items.length - 1);
break;
case "Escape":
event.preventDefault();
event.stopPropagation();
context.closeMenu();
break;
case "Tab":
context.closeMenu({ refocus: false });
break;
default:
break;
}
}
if (!open) return null;
return <div
ref={contentRef}
id={context.contentId}
data-ds-navbar-dropdown-content=""
aria-labelledby={rest["aria-label"] ? undefined : context.triggerId}
onKeyDown={handleKeyDown}
className={cx(
DROPDOWN_PANEL_CLASSES,
DROPDOWN_PLACEMENT_CLASSES[resolved],
!measured && "invisible",
className
)}
{...rest}
>
{children}
</div>;
}
function NavbarDropdownItem({
href,
active = false,
external = false,
disabled = false,
icon,
onSelect,
className,
children,
...rest
}) {
const context = useNavbarDropdown("NavbarDropdownItem");
const classes = cx(
DROPDOWN_ITEM_LAYOUT,
disabled ? LINK_DISABLED : active ? LINK_ACTIVE : LINK_IDLE,
className
);
const iconSlot = icon ? <span
aria-hidden="true"
className={cx(
"inline-flex shrink-0 [&_svg]:size-4",
active ? "text-current" : "text-[var(--ds-color-muted-foreground)]"
)}
>
{icon}
</span> : null;
const label = <span className="min-w-0 flex-1 truncate">{children}</span>;
const externalMarker = external ? <>
<ExternalLinkIcon className="size-3.5 shrink-0 text-[var(--ds-color-muted-foreground)]" />
<span className="sr-only">(opens in a new tab)</span>
</> : null;
if (disabled) {
return <span aria-disabled="true" className={classes}>
{iconSlot}
{label}
</span>;
}
if (href !== undefined) {
return <a
href={href}
aria-current={active ? "page" : undefined}
target={external ? "_blank" : undefined}
rel={external ? "noreferrer" : undefined}
onClick={() => {
onSelect?.();
context.closeMenu();
}}
className={classes}
{...rest}
>
{iconSlot}
{label}
{externalMarker}
</a>;
}
return <button
type="button"
onClick={() => {
onSelect?.();
context.closeMenu();
}}
className={classes}
{...rest}
>
{iconSlot}
{label}
</button>;
}
function NavbarDivider({ className, ...rest }) {
return <div
role="separator"
aria-orientation="horizontal"
className={cx(DIVIDER_CLASSES, className)}
{...rest}
/>;
}
export { Navbar, NavbarBrand, NavbarContent, NavbarSection, NavbarItem, NavbarLink, NavbarAction, NavbarToggle, NavbarMobile, NavbarMobileContent, NavbarDropdown, NavbarDropdownTrigger, NavbarDropdownContent, NavbarDropdownItem, NavbarDivider }; # Transparent Navbar
A navbar intended to sit over a page header or hero: the surface and bottom border are removed through the variant prop and the semantic token system, with no gradients, no glassmorphism, and no backdrop blur.
## Installation
Copy `code.tsx` (TypeScript) or `code.jsx` (plain JavaScript) into your project — it is a single self-contained module with no dependencies beyond React. Make sure your app loads Tailwind CSS and the DevSnips `--ds-*` design tokens (see [React/DESIGN_TOKENS.md](../../../DESIGN_TOKENS.md)); the component consumes the tokens through Tailwind arbitrary values such as `bg-[var(--ds-color-surface)]`. No component-specific CSS file is required.
## Usage
```tsx
import {
Navbar, NavbarBrand, NavbarContent, NavbarSection, NavbarItem,
NavbarLink, NavbarToggle, NavbarMobile, NavbarMobileContent,
} from "./navbar";
// `variant="transparent"` removes the surface + border; position it over
// the page header with absolute utilities and pad the header accordingly.
<div className="relative">
<Navbar variant="transparent" className="absolute inset-x-0 top-0 z-40">
<NavbarBrand href="/">Forge</NavbarBrand>
<NavbarContent>…</NavbarContent>
<NavbarToggle />
<NavbarMobile>…</NavbarMobile>
</Navbar>
<header className="border-b border-[var(--ds-color-border)] bg-[var(--ds-color-surface-subtle)] px-6 pb-16 pt-28">
…hero content…
</header>
</div>
// In a real app, switch to variant="default" (or add the surface classes)
// once the header scrolls away — the two variants share every token, so the
// swap is a single prop.
```
## JavaScript
A `code.jsx` build is provided for projects that ship plain JSX. It exposes the same API and behavior as `code.tsx` — only the TypeScript types are removed.
```jsx
import {
Navbar, NavbarBrand, NavbarContent, NavbarSection, NavbarItem,
NavbarLink, NavbarToggle, NavbarMobile, NavbarMobileContent,
} from "./navbar";
// `variant="transparent"` removes the surface + border; position it over
// the page header with absolute utilities and pad the header accordingly.
<div className="relative">
<Navbar variant="transparent" className="absolute inset-x-0 top-0 z-40">
<NavbarBrand href="/">Forge</NavbarBrand>
<NavbarContent>…</NavbarContent>
<NavbarToggle />
<NavbarMobile>…</NavbarMobile>
</Navbar>
<header className="border-b border-[var(--ds-color-border)] bg-[var(--ds-color-surface-subtle)] px-6 pb-16 pt-28">
…hero content…
</header>
</div>
// In a real app, switch to variant="default" (or add the surface classes)
// once the header scrolls away — the two variants share every token, so the
// swap is a single prop.
```
## Props
### `<Navbar>`
| Name | Type | Default | Description |
|---|---|---|---|
| `open` | `boolean` | — | Mobile-menu open state (controlled). |
| `defaultOpen` | `boolean` | `false` | Initial mobile-menu open state (uncontrolled). |
| `onOpenChange` | `(open: boolean) => void` | — | Called whenever the mobile menu requests to open or close. |
| `label` | `string` | `"Main"` | Accessible name of the `<nav>` landmark. |
| `breakpoint` | `"sm" \| "md" \| "lg"` | `"md"` | Breakpoint below which the desktop content collapses into the mobile navigation. |
| `variant` | `"default" \| "transparent"` | `"default"` | `transparent` removes the surface + bottom border for use over a page header. |
| `className` | `string` | — | Extra classes on the `<nav>` (e.g. `sticky top-0 z-40`). |
| `children` | `ReactNode` | — | Brand, content, toggle, and mobile region. |
### `<NavbarBrand>`
| Name | Type | Default | Description |
|---|---|---|---|
| `href` | `string` | `"/"` | Home URL the brand points at. |
| `className` | `string` | — | Extra classes (e.g. desktop centering for the centered pattern). |
| `children` | `ReactNode` | — | Any brand content: logo mark, wordmark, or both. |
A real `<a>`; every native anchor attribute is forwarded.
### `<NavbarContent>`
| Name | Type | Default | Description |
|---|---|---|---|
| `className` | `string` | — | Extra classes on the desktop content row. |
| `children` | `ReactNode` | — | `NavbarSection` regions (and, for the centered pattern, the brand). |
Hidden below the root `breakpoint` via a Tailwind responsive utility.
### `<NavbarSection>`
| Name | Type | Default | Description |
|---|---|---|---|
| `align` | `"start" \| "center" \| "end"` | `"start"` | Region of the bar: after the brand, centered, or trailing. |
| `className` | `string` | — | Extra classes on the region. |
| `children` | `ReactNode` | — | `NavbarItem` list items. |
Renders a `<ul role="list">` so the navigation region keeps list semantics.
### `<NavbarItem>`
| Name | Type | Default | Description |
|---|---|---|---|
| `className` | `string` | — | Extra classes on the item. |
| `children` | `ReactNode` | — | One `NavbarLink`, `NavbarAction`, or `NavbarDropdown`. |
A plain `<li>` wrapper — links, actions, and dropdowns are list items in both the desktop sections and the mobile region.
### `<NavbarLink>`
| Name | Type | Default | Description |
|---|---|---|---|
| `href` | `string` | `"#"` | Navigation target. |
| `active` | `boolean` | `false` | Current page: `aria-current="page"` + the active surface. |
| `external` | `boolean` | `false` | Opens in a new tab (`target="_blank" rel="noreferrer"`) with a visible + sr-only indicator. |
| `disabled` | `boolean` | `false` | Renders a non-interactive `aria-disabled` span — never a dead anchor. |
| `className` | `string` | — | Extra classes. |
| `children` | `ReactNode` | — | Visible label. |
A real `<a>`. Inside `NavbarMobileContent` it automatically switches to full-width stacked styling; activating it also closes an open mobile menu.
### `<NavbarAction>`
| Name | Type | Default | Description |
|---|---|---|---|
| `variant` | `"primary" \| "outline" \| "ghost"` | `"primary"` | Visual weight. |
| `href` | `string` | — | When present, renders a real `<a>` (e.g. a "Get started" link); otherwise a real `<button type="button">`. |
| `className` | `string` | — | Extra classes. |
| `children` | `ReactNode` | — | Visible label. |
Bar-height (36px) action sharing the Buttons family's primary/outline/ghost language. Native button or anchor attributes are forwarded.
### `<NavbarToggle>`
| Name | Type | Default | Description |
|---|---|---|---|
| `label` | `string` | `"Open/Close navigation menu"` (state-dependent) | Accessible name override. |
| `className` | `string` | — | Extra classes. |
A real `<button type="button">` with `aria-expanded` and `aria-controls` pointing at the mobile region; visible only below the root `breakpoint`. The hamburger/close icon swaps with state (aria-hidden).
### `<NavbarMobile>`
| Name | Type | Default | Description |
|---|---|---|---|
| `placement` | `"panel" \| "side"` | `"panel"` | `panel`: full-width disclosure under the bar. `side`: compact side panel with overlay, body scroll lock, and focus-on-open. |
| `className` | `string` | — | Extra classes on the region. |
| `children` | `ReactNode` | — | `NavbarMobileContent` (plus, for `side`, an optional header row). |
Rendered only while the mobile menu is open; the element carries the id the toggle's `aria-controls` points at. Hidden at and above the root `breakpoint`.
### `<NavbarMobileContent>`
| Name | Type | Default | Description |
|---|---|---|---|
| `className` | `string` | — | Extra classes on the list. |
| `children` | `ReactNode` | — | `NavbarItem` list items. |
A `<ul role="list">`; marks its subtree as the mobile area so `NavbarLink` renders stacked full-width.
### `<NavbarDropdown>`
| Name | Type | Default | Description |
|---|---|---|---|
| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled — the dropdown manages itself). |
| `placement` | `"bottom-start" \| "bottom-end"` | `"bottom-start"` | Panel alignment relative to the trigger; flips horizontally to stay in the viewport. |
| `className` | `string` | — | Extra classes on the relative wrapper. |
| `children` | `ReactNode` | — | `NavbarDropdownTrigger` + `NavbarDropdownContent`. |
### `<NavbarDropdownTrigger>`
| Name | Type | Default | Description |
|---|---|---|---|
| `icon` | `ReactNode` | — | Meaningful leading icon (rendered aria-hidden). |
| `className` | `string` | — | Extra classes. |
| `children` | `ReactNode` | — | Visible trigger label (a chevron is rendered after it). |
A real `<button type="button">` styled as a nav link, with `aria-haspopup="true"`, `aria-expanded`, and `aria-controls`. Click toggles; ArrowDown opens with the first item focused, ArrowUp with the last. Native button attributes (e.g. `aria-label` for icon-forward triggers) are forwarded.
### `<NavbarDropdownContent>`
| Name | Type | Default | Description |
|---|---|---|---|
| `aria-label` | `string` | — | Explicit accessible name; otherwise the panel is labelled by its trigger. |
| `className` | `string` | — | Extra classes (e.g. a wider `w-[min(36rem,100vw-2rem)]` for a mega menu). |
| `children` | `ReactNode` | — | `NavbarDropdownItem` entries, `NavbarDivider`, or grouped columns. |
Rendered only while open. Measures itself before paint and flips start ↔ end to stay in the viewport.
### `<NavbarDropdownItem>`
| Name | Type | Default | Description |
|---|---|---|---|
| `href` | `string` | — | Navigation target. When omitted, the item renders a `<button>` action instead of an anchor. |
| `active` | `boolean` | `false` | Current page: `aria-current="page"` + the active surface. |
| `external` | `boolean` | `false` | Opens in a new tab with a visible + sr-only indicator. |
| `disabled` | `boolean` | `false` | Non-interactive `aria-disabled` span — skipped by arrow keys. |
| `icon` | `ReactNode` | — | Meaningful leading icon (rendered aria-hidden). |
| `onSelect` | `() => void` | — | Called on activation before the dropdown closes. |
| `aria-label` | `string` | — | Accessible name override. |
| `children` | `ReactNode` | — | Visible label. |
### `<NavbarDivider>`
A `role="separator"` horizontal rule between dropdown groups. No props beyond `className`.
## Compound Components
Navbar is a compound component. Fifteen primitives compose the pattern:
```tsx
<Navbar>
<NavbarBrand href="/">Forge</NavbarBrand>
<NavbarContent>
<NavbarSection align="start">
<NavbarItem><NavbarLink href="/docs" active>Docs</NavbarLink></NavbarItem>
<NavbarItem><NavbarLink href="/pricing">Pricing</NavbarLink></NavbarItem>
</NavbarSection>
<NavbarSection align="end">
<NavbarItem><NavbarAction variant="ghost">Sign in</NavbarAction></NavbarItem>
<NavbarItem><NavbarAction variant="primary" href="/signup">Get started</NavbarAction></NavbarItem>
</NavbarSection>
</NavbarContent>
<NavbarToggle />
<NavbarMobile>
<NavbarMobileContent>
<NavbarItem><NavbarLink href="/docs" active>Docs</NavbarLink></NavbarItem>
<NavbarItem><NavbarLink href="/pricing">Pricing</NavbarLink></NavbarItem>
</NavbarMobileContent>
</NavbarMobile>
</Navbar>
```
- `Navbar` — the root `<nav>` landmark. Owns the mobile-menu state (controlled via `open` + `onOpenChange`, or uncontrolled via `defaultOpen`), the landmark label, the responsive `breakpoint`, and the `default` / `transparent` surface variant.
- `NavbarBrand` — a real `<a>` home link wrapping any ReactNode brand (logo mark, wordmark, or both).
- `NavbarContent` — the desktop content row, hidden below the breakpoint. Contains `NavbarSection` regions.
- `NavbarSection` — a `<ul>` region aligned `start`, `center`, or `end`; its children are `NavbarItem` list items.
- `NavbarItem` — a `<li>` wrapping one link, action, or dropdown.
- `NavbarLink` — a real `<a>` navigation link with `active` (`aria-current="page"`), `external` (`target="_blank"` + indicator), and `disabled` (non-interactive `aria-disabled` span — never a dead anchor).
- `NavbarAction` — a bar-height action: a real `<button>` by default, a real `<a>` when `href` is passed. `primary` / `outline` / `ghost` variants.
- `NavbarToggle` — the mobile-menu button: `aria-expanded`, `aria-controls` pointing at the mobile region, dynamic accessible name, hamburger/close icon swap.
- `NavbarMobile` — the collapsible mobile region referenced by `aria-controls`. `placement="panel"` (full-width disclosure under the bar) or `placement="side"` (compact side panel with overlay, scroll lock, and focus-on-open).
- `NavbarMobileContent` — the `<ul>` inside the mobile region; links inside it automatically switch to full-width stacked styling.
- `NavbarDropdown` — a navigation dropdown root (disclosure pattern). Owns its open state and panel placement.
- `NavbarDropdownTrigger` — a real `<button>` styled as a nav link, with `aria-haspopup="true"`, `aria-expanded`, `aria-controls`, and a rotating chevron.
- `NavbarDropdownContent` — the absolutely positioned panel, labelled by its trigger. Rendered only while open; flips its alignment to stay in the viewport.
- `NavbarDropdownItem` — one entry: a real `<a>` when `href` is passed, otherwise a real `<button>` action. Supports `active`, `external`, `disabled`, `icon`, and `onSelect`.
- `NavbarDivider` — a `role="separator"` rule between dropdown groups.
`Navbar variant="transparent"` swaps the root's surface classes for `bg-transparent border-transparent` — a first-class prop, not a className override, so there are no conflicting utilities. Positioning (`absolute inset-x-0 top-0 z-40`) is composition on the same root.
## Navigation Behavior
The transparent variant demonstrates how the token system handles an alternate surface: nothing about the links, actions, focus rings, or dropdown panels changes — they already reference semantic tokens (`surface-hover`, `surface-active`, `foreground`, `border`), which read correctly on any quiet background. The header below uses a flat `surface-subtle` fill with a hairline border — deliberately no gradients, no glassmorphism, no backdrop blur.
Because the navbar is absolutely positioned over the header, the header supplies its own top padding (`pt-28`) so content never runs under the bar. When the mobile menu opens, its panel keeps the SOLID surface (a transparent floating panel over content would be unreadable) — only the bar itself is transparent.
In a real product, pair this with a scroll position or route change that swaps `variant="transparent"` → `variant="default"`; the swap is one prop and both variants share geometry, so there is no layout shift.
## Keyboard Interaction
| Key | Context | Behavior |
|---|---|---|
| `Tab` / `Shift+Tab` | bar | Move through brand, links, actions, dropdown triggers, and the toggle in DOM order |
| `Enter` / `Space` | dropdown trigger | Toggle the dropdown; focus moves to the first item |
| `ArrowDown` | dropdown trigger | Open the dropdown, focus the first item |
| `ArrowUp` | dropdown trigger | Open the dropdown, focus the last item |
| `ArrowDown` / `ArrowUp` | dropdown panel | Move focus to the next / previous enabled item, wrapping at the ends |
| `Home` / `End` | dropdown panel | Focus the first / last enabled item |
| `Enter` | link / item | Follow the link / activate the item (native behavior) |
| `Escape` | dropdown panel | Close the dropdown and return focus to its trigger |
| `Tab` | dropdown panel | Close the dropdown and move focus forward naturally |
| `Escape` | anywhere (mobile menu open) | Close the mobile navigation and return focus to the toggle |
The trigger, items, and toggle are native `<button>` / `<a>` elements, so Enter/Space activation and Tab order follow normal browser behavior. Disabled entries use non-interactive `aria-disabled` spans: they are skipped by arrow-key navigation and removed from the tab order. Focus is never trapped — the mobile navigation is a disclosure, not a modal dialog.
## Accessibility
The structure follows the WAI-ARIA disclosure navigation pattern.
- The root is a semantic `<nav>` landmark with an accessible name (`label`, default "Main") — pass a distinct label when more than one navbar is on the page.
- Navigation links are real `<a href>` elements (normal browser navigation, middle-click, and screen-reader link semantics); actions and toggles are real `<button>` elements. No `div` click handlers, no nested interactive elements.
- The mobile toggle is a real `<button>` with `aria-expanded` and `aria-controls` pointing at the actual mobile region; its accessible name reflects the state ("Open/Close navigation menu").
- Dropdown triggers carry `aria-haspopup="true"` + `aria-expanded` + `aria-controls`; the panel is labelled by its trigger. Navigation dropdowns intentionally do NOT use `role="menu"`/`role="menuitem"` — the panel contains real links, and the ARIA menu pattern is for action menus, not navigation.
- The mobile navigation is NOT a modal dialog: focus is never trapped. The `side` placement moves focus into the panel on open and restores it to the toggle on close, but Tab always moves forward naturally.
- Disabled links and dropdown items render as non-interactive spans with `aria-disabled="true"` — they are skipped by arrow keys, removed from the tab order, and never presented as followable links.
- External links announce themselves with `target="_blank" rel="noreferrer"`, a visible (aria-hidden) indicator glyph, and screen-reader-only "(opens in a new tab)" text.
- Every interactive element has a visible `focus-visible` ring via the `--ds-color-focus-ring` token, and all transitions are disabled under `prefers-reduced-motion`.
Removing the surface changes visuals only: the landmark, contrast (foreground tokens on the quiet header surface), focus rings, and mobile disclosure are unchanged. The mobile panel keeps a solid surface token so menu content never sits directly on imagery.
## Active Navigation
Pass `active` to the `NavbarLink` or `NavbarDropdownItem` that represents the current page. Active items render with the `--ds-color-surface-active` background and foreground text (background + color, never color alone) and expose `aria-current="page"` to assistive technology. In a routed app, derive `active` from the current route:
```tsx
<NavbarLink href="/docs" active={pathname.startsWith("/docs")}>Docs</NavbarLink>
```
Exactly one item in a navigation region should be current at a time.
## Responsive Behavior
The family collapses by breakpoint, not by JavaScript width detection: below the configured `breakpoint` (`sm` / `md` / `lg`, default `md`) `NavbarContent` is hidden with a Tailwind responsive utility and the `NavbarToggle` appears; the `NavbarMobile` region is likewise hidden at and above the breakpoint. No resize listeners are involved.
- The bar is a single 56px row (`h-14`) with `max-w-6xl` content width and fluid horizontal padding (`px-4 sm:px-6`); long link labels truncate (`min-w-0` + `truncate`) instead of forcing overflow.
- The `panel` mobile placement is absolutely positioned under the bar, so opening/closing it never shifts page layout; it caps its height at `100dvh - 4rem` and scrolls internally.
- Dropdown panels cap their width at `100vw - 1.5rem` and height at `min(24rem, 100dvh - 6rem)` with internal scrolling, and flip their horizontal alignment (start ↔ end) to stay inside the viewport.
- All controls keep comfortable touch targets: 36px (h-9) actions/toggles, 32px+ link hit areas.
Identical collapse behavior to the reference at 375/768/1280; the absolute positioning spans the full width at every breakpoint and the header's fluid padding keeps the hero clear of the bar.
## Controlled and Uncontrolled State
The mobile menu supports both state modes:
- **Uncontrolled** (default) — `<Navbar>` owns the state; optionally seed it with `defaultOpen`.
- **Controlled** — pass `open` + `onOpenChange`; the parent owns the state. Every internal request (toggle click, Escape, outside pointer, link activation) flows through `onOpenChange`.
```tsx
const [open, setOpen] = useState(false);
<Navbar open={open} onOpenChange={setOpen}>…</Navbar>
```
`NavbarDropdown` manages its own open state internally (seed with `defaultOpen`); it closes itself on selection, Escape, Tab, or outside pointer interaction, and restores focus to its trigger.
The mobile menu is uncontrolled.
## Styling
Built with React, Tailwind CSS, and DevSnips design tokens. The component consumes the `--ds-*` semantic tokens via arbitrary values (for example `bg-[var(--ds-color-surface)]`). Define the tokens once in your theme — no component-specific CSS file is required.
## Design Tokens
See [React/DESIGN_TOKENS.md](../../../DESIGN_TOKENS.md) for the authoritative token specification. This navbar variant uses the semantic color, radius, shadow, typography, and motion tokens, and follows the navigation rules (48–56px top-nav height, subtle bottom border, quiet state changes).
## Notes and Limitations
The transparent treatment is only as good as what sits under it: use it over quiet, low-contrast headers (flat surfaces, hairlines, type). Over photography or busy artwork, keep the default surface — no overlay treatment makes that accessible.
- The desktop content collapses purely through Tailwind responsive utilities at the configured `breakpoint` (default `md`); there is no JavaScript width detection. If the viewport is resized past the breakpoint while the mobile menu is open, the region hides visually while the state remains open — close it via the toggle or Escape before resizing, or manage `open` yourself.
- The mobile navigation is a disclosure, not a dialog: focus is never trapped, even in the `side` placement. If you need a true modal navigation drawer, compose the DevSnips Dialog family instead.
- Dropdown panels anchor to their trigger with `absolute` positioning inside a `relative` wrapper — no positioning library. The viewport flip covers horizontal overflow; a navbar at the very bottom edge of a short viewport can still clip a tall panel vertically (the panel caps its height and scrolls internally instead). 1002 lines UTF-8 · LF · Spaces: 2
DevSnips AI ready Claude · Cursor · Windsurf
Agent skill & Model Context Protocol
Pass this component straight into your AI workflow. DevSnips exposes each resource as a structured, addressable registry entry, so your agent installs the real files instead of approximating them.
devsnips://components/navbar/navbar-transparent I want to use this DevSnips resource in production: Page: https://devsnips.site/library/react/components/navbar/navbar-transparent/ Install: npx devsnips add React/Components/Navbar/navbar-transparent This is the Transparent Navbar — A navbar intended to sit over a page header or hero: the surface and bottom border are removed through the variant prop and the semantic token system, with no gradients, no glassmorphism, and no backdrop blur. Use this DevSnips resource as the starting point for my project. Adapt it to my project's stack and conventions, fix any issues, and make it production-ready while preserving its intended design and behavior.
Paste into ChatGPT, Claude or your agent — it already carries the page URL and the exact install path.
Resource URI
devsnips://components/navbar/navbar-transparent