Skip to content
Black FridayBlack Friday launch price: Lifetime $149 until 30 Nov. Regular price $299 from 1 Dec.ends in 15d 03:13:35Get Lifetime →
loopkit
freeNew228 lines · framer-motion

Navbar Centered Logo

The classic bar: links left, links right, the mark right in the middle. A classic marketing nav with the logo centred between a left and a right group of links and a CTA on the far right, sitting above a thin low-opacity divider. Below 768px the link groups collapse into a hamburger (the logo stays centred) that opens a sheet listing every link.

Use it for
Marketing home pages and docs sites that want a traditional, legible nav with the brand mark as the visual anchor, and a usable version on phones.
Themes
Light + dark
License
Commercial use
Live

A home page that explains itself in one scroll.

Plan, build and launch sit in the same bar, so visitors always know where they are.

Plan

Lay the quarter out before anyone opens a doc.

Build

Ship changes in small, reviewable steps.

Launch

Tell everyone what changed, in plain language.

tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";

function useReducedMotionSafe() {
  const prefersReduced = useReducedMotion();
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => setMounted(true), []);
  return mounted ? !!prefersReduced : false;
}

/**
 * A classic nav with the logo centred between two link groups and a CTA on
 * the far right, sitting above a thin low-opacity divider. Below 768px the
 * link groups collapse into a hamburger (logo stays centred) that opens a
 * sheet of all links, positioned `absolute` under the bar inside the section.
 */

export interface NavLink {
  label: string;
  href?: string;
}

export interface NavbarCenteredLogoProps {
  logoText?: string;
  leftLinks?: NavLink[];
  rightLinks?: NavLink[];
  ctaLabel?: string;
  ctaHref?: string;
  showCta?: boolean;
  headline?: string;
  subcopy?: string;
  /** Render the page content behind the bar. Turn off to render just the bar. */
  showBackdrop?: boolean;
  className?: string;
}

const DEFAULT_LEFT: NavLink[] = [{ label: "Product" }, { label: "Solutions" }, { label: "Pricing" }];
const DEFAULT_RIGHT: NavLink[] = [{ label: "Docs" }, { label: "Changelog" }, { label: "Support" }];

function Mark({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 32 32" className={cn("size-7", className)} aria-hidden>
      <rect width="32" height="32" rx="16" className="fill-primary" />
      <path d="M11 20.5c0-4.7 2.5-8.5 5-8.5s5 3.8 5 8.5" fill="none" className="stroke-primary-foreground" strokeWidth="2.2" strokeLinecap="round" />
      <circle cx="16" cy="11" r="1.7" className="fill-signal" />
    </svg>
  );
}

function Hamburger({ open }: { open: boolean }) {
  return (
    <svg viewBox="0 0 24 24" className="size-5" aria-hidden>
      <motion.line x1="4" x2="20" strokeWidth="2" strokeLinecap="round" className="stroke-current" animate={{ y1: open ? 12 : 7, y2: open ? 12 : 7, rotate: open ? 45 : 0 }} style={{ originX: "12px", originY: "12px" }} transition={{ duration: 0.2 }} />
      <motion.line x1="4" x2="20" y1="17" y2="17" strokeWidth="2" strokeLinecap="round" className="stroke-current" animate={{ opacity: open ? 0 : 1 }} transition={{ duration: 0.15 }} />
      <motion.line x1="4" x2="20" strokeWidth="2" strokeLinecap="round" className="stroke-current" animate={{ y1: open ? 12 : 17, y2: open ? 12 : 17, rotate: open ? -45 : 0 }} style={{ originX: "12px", originY: "12px" }} transition={{ duration: 0.2 }} />
    </svg>
  );
}

export function NavbarCenteredLogo({
  logoText = "Fluxa",
  leftLinks = DEFAULT_LEFT,
  rightLinks = DEFAULT_RIGHT,
  ctaLabel = "Sign in",
  ctaHref = "#",
  showCta = true,
  headline = "A home page that explains itself in one scroll.",
  subcopy = "Plan, build and launch sit in the same bar, so visitors always know where they are.",
  showBackdrop = true,
  className,
}: NavbarCenteredLogoProps) {
  const reduce = useReducedMotionSafe();
  const [open, setOpen] = React.useState(false);
  const toggleRef = React.useRef<HTMLButtonElement>(null);

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        setOpen(false);
        toggleRef.current?.focus();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open]);

  const allLinks = [...leftLinks, ...rightLinks];

  const features = [
    { label: "Plan", body: "Lay the quarter out before anyone opens a doc." },
    { label: "Build", body: "Ship changes in small, reviewable steps." },
    { label: "Launch", body: "Tell everyone what changed, in plain language." },
  ];

  return (
    <section className={cn("relative w-full bg-background", className)}>
      <div className="relative mx-auto max-w-6xl px-4 sm:px-6 lg:px-10">
        <nav className="relative z-40 flex h-20 items-center" aria-label="Primary">
          <div className="flex flex-1 items-center gap-2">
            <button
              ref={toggleRef}
              type="button"
              aria-expanded={open}
              aria-controls="centered-logo-sheet"
              onClick={() => setOpen((v) => !v)}
              className="grid size-10 place-items-center rounded-full border border-border text-foreground transition hover:bg-muted md:hidden"
            >
              <Hamburger open={open} />
              <span className="sr-only">Menu</span>
            </button>
            <div className="hidden items-center gap-7 md:flex">
              {leftLinks.map((l) => (
                <a key={l.label} href={l.href ?? "#"} onClick={(e) => (!l.href || l.href === "#") && e.preventDefault()} className="text-sm font-semibold text-foreground/80 transition hover:text-foreground">
                  {l.label}
                </a>
              ))}
            </div>
          </div>

          <a
            href="#"
            onClick={(e) => e.preventDefault()}
            className="absolute left-1/2 -translate-x-1/2 inline-flex items-center gap-2 font-display text-[1.2rem] font-extrabold tracking-[-0.04em] text-foreground"
          >
            <Mark />
            {logoText}
          </a>

          <div className="flex flex-1 items-center justify-end gap-6">
            <div className="hidden items-center gap-7 md:flex">
              {rightLinks.map((l) => (
                <a key={l.label} href={l.href ?? "#"} onClick={(e) => (!l.href || l.href === "#") && e.preventDefault()} className="text-sm font-semibold text-foreground/80 transition hover:text-foreground">
                  {l.label}
                </a>
              ))}
            </div>
            {showCta && (
              <a
                href={ctaHref}
                onClick={(e) => (!ctaHref || ctaHref === "#") && e.preventDefault()}
                className="hidden h-10 items-center rounded-full bg-primary px-5 text-sm font-semibold text-primary-foreground transition hover:opacity-90 md:inline-flex"
              >
                {ctaLabel}
              </a>
            )}
            <span className="size-10 md:hidden" aria-hidden />
          </div>
        </nav>

        <AnimatePresence>
          {open && (
            <motion.div
              id="centered-logo-sheet"
              role="dialog"
              aria-modal="true"
              aria-label="Site navigation"
              className="absolute inset-x-0 top-full z-30 rounded-b-2xl border border-t-0 border-border bg-card p-3 shadow-xl shadow-black/10 md:hidden"
              initial={{ opacity: 0, y: reduce ? 0 : -8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: reduce ? 0 : -8 }}
              transition={{ duration: reduce ? 0.12 : 0.2, ease: [0.16, 1, 0.3, 1] }}
            >
              <nav className="flex flex-col" aria-label="All links">
                {allLinks.map((l) => (
                  <a
                    key={l.label}
                    href={l.href ?? "#"}
                    onClick={(e) => {
                      if (!l.href || l.href === "#") e.preventDefault();
                      setOpen(false);
                      toggleRef.current?.focus();
                    }}
                    className="rounded-xl px-3 py-3 text-[15px] font-semibold text-foreground transition hover:bg-muted"
                  >
                    {l.label}
                  </a>
                ))}
              </nav>
              {showCta && (
                <a
                  href={ctaHref}
                  onClick={(e) => {
                    if (!ctaHref || ctaHref === "#") e.preventDefault();
                    setOpen(false);
                  }}
                  className="mt-2 inline-flex h-11 items-center justify-center rounded-full bg-primary px-5 text-sm font-semibold text-primary-foreground transition hover:opacity-90"
                >
                  {ctaLabel}
                </a>
              )}
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      {/* outside-click catcher for the mobile sheet, sized to the section only */}
      {open && <button type="button" aria-hidden tabIndex={-1} onClick={() => setOpen(false)} className="absolute inset-0 z-20 cursor-default md:hidden" />}

      {showBackdrop && <div className="relative h-px w-full bg-border/50" aria-hidden />}

      {showBackdrop && (
        <div className="relative mx-auto max-w-5xl px-4 py-16 text-center sm:px-6 sm:py-20 lg:px-10">
          <h2 className="mx-auto max-w-xl font-display text-[clamp(1.7rem,3.8vw,2.6rem)] font-extrabold leading-[1.05] tracking-[-0.03em] text-foreground">{headline}</h2>
          <p className="mx-auto mt-3 max-w-md text-sm leading-relaxed text-muted-foreground">{subcopy}</p>

          <div className="mx-auto mt-9 grid max-w-3xl grid-cols-1 gap-3 sm:grid-cols-3">
            {features.map((f, i) => (
              <div key={f.label} className="rounded-2xl border border-border bg-card p-5 text-left">
                <div className={cn("mb-3 flex size-8 items-center justify-center rounded-lg", i === 0 ? "bg-signal/15 text-signal" : i === 1 ? "bg-info/15 text-info" : "bg-ok/15 text-ok")}>
                  <span className="size-2.5 rounded-full bg-current" />
                </div>
                <p className="text-sm font-semibold text-foreground">{f.label}</p>
                <p className="mt-1 text-xs leading-relaxed text-muted-foreground">{f.body}</p>
              </div>
            ))}
          </div>
        </div>
      )}
    </section>
  );
}

export default NavbarCenteredLogo;

How it behaves

  • →Logo is absolutely centred inside the nav independent of how wide the left/right link groups are, using absolute left-1/2 -translate-x-1/2 anchored to the nav bar itself.
  • →At 768px and above the left/right link groups and CTA show as md:flex; below that they're hidden and a hamburger button fades in on the left, keeping the centred logo.
  • →The hamburger opens a sheet positioned absolute inset-x-0 top-full under the nav bar (inside the section, not fixed or portaled) listing every link from both groups plus the CTA.
  • →An invisible full-section click-catcher closes the sheet on outside click; Escape also closes it and returns focus to the hamburger.
  • →showCta toggles the CTA button on both the desktop bar and the mobile sheet live.
  • →Only finite enter/exit transitions are used (icon morph, sheet fade/slide) — no continuous loop, so prefers-reduced-motion only shortens those transitions, nothing to replace with a static frame.

Accessibility

  • ✓Hamburger button has `aria-expanded` and `aria-controls` pointing at the sheet; sheet has `role="dialog"` and `aria-label`.
  • ✓Escape closes the sheet and restores focus to the hamburger button; every link and the CTA stay real `<a>` elements in the tab order.
  • ✓The outside-click catcher is `aria-hidden` and `tabIndex={-1}` so it never appears in the accessibility tree or tab order.
  • ✓Honours prefers-reduced-motion.