Navbar
Preview
SolidFilled surface, no rim.
Usage
import { Navbar } from "@/components/library/navbar"; export default function Example() { return <Navbar version="solid" />;}
navbar.tsx
"use client"; import { type MouseEvent, useState } from "react";import { ThemeToggle } from "@/components/theme/theme-toggle";import { cn } from "@/lib/cn"; export type NavbarVersion = "border" | "solid" | "blur"; /** * ──────────────────────────────────────────────────────────────── * SHAPE — shared by every version, never changes between them. * * FLARE concave radius where the bar meets the top edge. The sides * curve *outwards* into the ceiling, like the macOS notch. * RADIUS convex radius on the two bottom corners. * ──────────────────────────────────────────────────────────────── */const FLARE = 24;const RADIUS = 22; /** * Carves the concave corner: everything within FLARE of the wing's inner * bottom corner is masked away, leaving the arc that meets the bar's side * vertically and the ceiling horizontally. */function flareMask(side: "left" | "right") { const origin = side === "left" ? "0% 100%" : "100% 100%"; return `radial-gradient(circle at ${origin}, transparent ${FLARE}px, #000 ${FLARE + 0.5}px)`;} /** * ──────────────────────────────────────────────────────────────── * VERSIONS * Only the fill changes — background, backdrop and outline. * The silhouette above is identical in all of them. * ──────────────────────────────────────────────────────────────── */const STYLES = { border: { fill: "bg-background", // Traces a 1px rim around the whole silhouette, curves included. outline: "[filter:drop-shadow(0_1px_0_var(--border))_drop-shadow(0_-1px_0_var(--border))_drop-shadow(1px_0_0_var(--border))_drop-shadow(-1px_0_0_var(--border))]", }, solid: { fill: "bg-surface-2", outline: "", }, blur: { fill: "bg-muted/80 backdrop-blur-3xl", outline: "", },} satisfies Record<NavbarVersion, Record<string, string>>; const NAV = ["Work", "Components", "Contact"]; const DEFAULT_LABELS = { nav: NAV, contact: "Contact", getInTouch: "Get in touch", browse: "Browse components", mainLabel: "Main", menuLabel: "Toggle menu",}; const MENU_ID = "library-navbar-menu"; export function Navbar({ version = "border", position = "fixed", layout = "responsive", locale = "en", labels = DEFAULT_LABELS, className,}: { version?: NavbarVersion; /** * `absolute` pins the bar to its nearest positioned ancestor instead of the * viewport — that is what keeps it inside a preview box. */ position?: "fixed" | "absolute"; /** * `mobile` holds the small-screen bar at every width — logo, one call to * action, menu button — for places too narrow to earn the full layout. */ layout?: "responsive" | "mobile"; locale?: "en" | "es"; labels?: { nav: readonly string[]; contact: string; getInTouch: string; browse: string; mainLabel: string; menuLabel: string; }; className?: string;}) { const s = STYLES[version]; const [open, setOpen] = useState(false); const tight = layout === "mobile"; // Demo links: they navigate nowhere. const hold = (event: MouseEvent<HTMLAnchorElement>) => event.preventDefault(); return ( <div className={cn( "inset-x-0 top-0 z-50 flex w-full flex-col items-center px-6", // Spelled out so Tailwind sees both classes in the source. position === "absolute" ? "absolute" : "fixed", className, )} > <div className="relative w-full max-w-3xl"> {/* Shape layer. Kept free of text so the outline filter traces only the silhouette and never haloes the content. It spans the bar *and* the drawer below it, so opening the menu simply stretches the same silhouette — the flares are pinned to `top-0` at a fixed size, which leaves the concave corners untouched however tall the bar grows. */} <div aria-hidden="true" className={cn("absolute inset-0", s.outline)}> <span className={cn("absolute top-0 right-full", s.fill)} style={{ width: FLARE, height: FLARE, maskImage: flareMask("left"), WebkitMaskImage: flareMask("left"), }} /> <span className={cn("absolute inset-0", s.fill)} style={{ borderRadius: `0 0 ${RADIUS}px ${RADIUS}px` }} /> <span className={cn("absolute top-0 left-full", s.fill)} style={{ width: FLARE, height: FLARE, maskImage: flareMask("right"), WebkitMaskImage: flareMask("right"), }} /> </div> {/* Content sits above the shape. */} <nav aria-label={labels.mainLabel} className="relative flex h-14 items-center gap-3 px-4 sm:gap-6" > <a href="#" onClick={hold} className="flex shrink-0 items-center gap-2.5" > <span className="text-xl font-medium tracking-tight text-foreground"> Monza<span className="text-primary">.</span> </span> </a> <ul className={cn("hidden items-center gap-1", !tight && "sm:flex")}> {labels.nav.map((link) => ( <li key={link}> <a href="#" onClick={hold} className="rounded-full px-3 py-1.5 text-[13px] text-muted-foreground transition-colors hover:text-foreground" > {link} </a> </li> ))} </ul> <div className="ml-auto flex items-center gap-2"> {tight ? ( <a href="#" onClick={hold} className="inline-flex h-7 shrink-0 items-center rounded-lg bg-primary px-3.5 text-[13px] font-semibold tracking-tight text-white transition-opacity hover:opacity-80" > {labels.contact} </a> ) : ( <> <ThemeToggle locale={locale} /> <a href="#" onClick={hold} className="hidden h-7 shrink-0 items-center rounded-lg bg-muted px-3.5 text-[13px] font-semibold tracking-tight text-foreground transition-opacity hover:opacity-80 hover:text-foreground sm:inline-flex" > {labels.getInTouch} </a> <a href="#" onClick={hold} className="hidden h-7 shrink-0 items-center rounded-lg bg-primary px-3.5 text-[13px] font-semibold tracking-tight text-white transition-opacity hover:opacity-80 hover:text-foreground sm:inline-flex" > {labels.browse} </a> </> )} <button type="button" onClick={() => setOpen((value) => !value)} aria-expanded={open} aria-controls={MENU_ID} aria-label={labels.menuLabel} className={cn( "inline-flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground", !tight && "sm:hidden", )} > {/* Two bars folding into an X. `justify-between` inside a 10px box leaves each bar 4px off centre — exactly the distance the translate closes before the rotation lands. */} <span aria-hidden="true" className="flex h-2.5 w-5 flex-col justify-between" > <span className={cn( "block h-0.5 w-full origin-center rounded-full bg-current transition-transform duration-300 ease-in-out motion-reduce:transition-none", open && "translate-y-[4px] rotate-45", )} /> <span className={cn( "block h-0.5 w-full origin-center rounded-full bg-current transition-transform duration-300 ease-in-out motion-reduce:transition-none", open && "-translate-y-[4px] -rotate-45", )} /> </span> </button> </div> </nav> {/* Drawer. Lives inside the shape box so the bar itself grows around it. `grid-rows-[0fr → 1fr]` animates the height without pinning a magic max-height; the inner `overflow-hidden` is what lets the row collapse past its content. */} <div id={MENU_ID} inert={!open} className={cn( "relative grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none", !tight && "sm:hidden", open ? "grid-rows-[1fr]" : "grid-rows-[0fr]", )} > <div className="overflow-hidden px-4"> <ul className="space-y-0.5 border-t border-border pt-2 pb-3"> {labels.nav.map((link, index) => ( <li key={link} className={cn( "transition-[opacity,transform] duration-300 ease-out motion-reduce:transition-none", open ? "translate-y-0 opacity-100" : "-translate-y-1 opacity-0", )} // Items trail the expansion on the way in, and leave at once. style={{ transitionDelay: open ? `${80 + index * 50}ms` : "0ms", }} > <a href="#" onClick={(event) => { hold(event); setOpen(false); }} className="block rounded-lg px-3 py-2.5 text-[15px] text-muted-foreground transition-colors hover:bg-foreground/10 hover:text-foreground" > {link} </a> </li> ))} </ul> </div> </div> </div> </div> );}
DependenciesNavbar imports these from the project. Copy them across too if you have no equivalent.
components/theme/theme-toggle.tsx
"use client"; import { Moon, Sun } from "reicon-react";import { LANDING_COPY, type Locale } from "@/lib/i18n";import { toggleTheme, useTheme } from "@/lib/theme"; export function ThemeToggle({ className = "", locale = "en",}: { className?: string; locale?: Locale;}) { const theme = useTheme(); const next = theme === "dark" ? "light" : "dark"; const label = LANDING_COPY[locale].theme[next]; return ( <button type="button" onClick={toggleTheme} aria-label={label} title={label} className={`inline-flex px-1.5 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:border-border-strong hover:text-foreground ${className}`.trim()} > <Sun size={16} weight="Filled" aria-hidden="true" className="hidden dark:block" /> <Moon size={16} weight="Filled" aria-hidden="true" className="block dark:hidden" /> </button> );}
lib/theme.ts
"use client"; import { useSyncExternalStore } from "react"; export type Theme = "light" | "dark"; const STORAGE_KEY = "theme"; /** * The theme lives on <html> — the blocking script in `layout.tsx` puts it * there before first paint. React subscribes to that DOM state rather than * owning a second copy of it. */const listeners = new Set<() => void>(); function emit() { for (const listener of listeners) listener();} function readStored(): Theme | null { try { const stored = localStorage.getItem(STORAGE_KEY); return stored === "light" || stored === "dark" ? stored : null; } catch { return null; }} function apply(theme: Theme) { const root = document.documentElement; const themeColor = theme === "dark" ? "#000000" : "#ffffff"; root.classList.toggle("dark", theme === "dark"); root.style.colorScheme = theme; root.style.backgroundColor = themeColor; document .querySelector<HTMLMetaElement>('meta[name="theme-color"]') ?.setAttribute("content", themeColor); document .querySelector<HTMLMetaElement>('meta[name="color-scheme"]') ?.setAttribute("content", theme); emit();} function getSnapshot(): Theme { return document.documentElement.classList.contains("dark") ? "dark" : "light";} /** Matches the script's fallback, so hydration renders what the server sent. */function getServerSnapshot(): Theme { return "dark";} let boundToOS = false; function subscribe(onStoreChange: () => void) { listeners.add(onStoreChange); // Follow the OS, but only until the visitor makes an explicit choice. if (!boundToOS) { boundToOS = true; const media = window.matchMedia("(prefers-color-scheme: dark)"); media.addEventListener("change", (event) => { if (readStored()) return; apply(event.matches ? "dark" : "light"); }); } return () => { listeners.delete(onStoreChange); };} export function setTheme(theme: Theme) { try { localStorage.setItem(STORAGE_KEY, theme); } catch { // Blocked storage: the theme still applies for this visit. } apply(theme);} export function toggleTheme() { setTheme(getSnapshot() === "dark" ? "light" : "dark");} export function useTheme(): Theme { return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);}
lib/i18n.ts
export type Locale = "en" | "es"; export const LANDING_COPY = { en: { nav: { about: "About", work: "Work", skills: "Skills", contact: "Get in touch", mainLabel: "Main navigation", menuLabel: "Toggle menu", languageLabel: "Ver sitio en español", }, hero: { welcome: "Welcome to my personal site", contact: "Get in touch", work: "Featured Work", productPrefix: "Product", productSuffix: "", rotatingWords: ["Designer", "Specialist", "Builder"], rotatingLabel: "Designer, Specialist and Builder", }, about: { intro: "I'm Matias Monzalvo", description: "22yo design engineer based in Buenos Aires. I turn complex problems into clear, useful digital products, moving from early strategy to shipped details and building the systems that keep every experience coherent as it grows.\nThis is the tool I work with every day.", facts: [ { value: "6 years", label: "Designing products" }, { value: "12+", label: "Featured Projects" }, { value: "Based in", label: "Buenos Aires, Argentina" }, ], }, work: { heading: "Featured Work", description: (count: number) => `Apps, sites, products and the systems behind them. +${count} projects shipped end to end.`, imageAlt: (title: string) => `Screenshot of ${title}`, }, formation: "None of it was luck. Every screen up there is the sum of what I studied, practised and rebuilt until it held — the training that turns a blank file into a system, and a system into something worth shipping.", skills: { heading: "Skills & Experience", description: "These are the tools, languages, and systems I use in my day-to-day work, along with the experiences that have shaped how I approach it.", }, contact: { heading: "Get in touch", description: "I'm open to working on innovative and ambitious projects with meaningful impact.", }, theme: { light: "Switch to light theme", dark: "Switch to dark theme", }, visuals: { aiLabel: "Claude Code, Codex and Cursor feeding into one pair of hands, and one result out the far side", education: [ "University", "App Flows", "C1 Cambridge", "Hackathons", "Courses", "Side Projects", "Sales", "Customer Support", ], }, footer: "Designed and built in the open.", componentShowcase: { versionLabel: "Component version", preview: "Preview", usage: "Usage", dependencies: "Dependencies", dependencyDescription: (name: string) => `${name} imports these from the project. Copy them across too if you have no equivalent.`, copy: "Copy", copied: "Copied", }, }, es: { nav: { about: "Sobre mí", work: "Proyectos", skills: "Habilidades", contact: "Contactame", mainLabel: "Navegación principal", menuLabel: "Abrir o cerrar el menú", languageLabel: "View site in English", }, hero: { welcome: "Bienvenido a mi sitio personal", contact: "Contactame", work: "Proyectos destacados", productPrefix: "", productSuffix: " de productos", rotatingWords: ["Diseñador", "Estratega", "Creador"], rotatingLabel: "Diseñador, estratega y creador", }, about: { intro: "Soy Matias Monzalvo", description: "Soy un design engineer de 22 años que vive en Buenos Aires. Convierto problemas complejos en productos digitales claros y útiles: avanzo desde la estrategia inicial hasta los detalles del lanzamiento y construyo los sistemas que mantienen cada experiencia coherente a medida que crece.\nEsta es la herramienta con la que trabajo todos los días.", facts: [ { value: "6 años", label: "Diseñando productos" }, { value: "12+", label: "Proyectos Destacados" }, { value: "Vivo en", label: "Buenos Aires, Argentina" }, ], }, work: { heading: "Proyectos destacados", description: (count: number) => `Apps, sitios, productos y los sistemas que los sostienen: +${count} proyectos desarrollados de principio a fin.`, imageAlt: (title: string) => `Captura de pantalla de ${title}`, }, formation: "Nada fue cuestión de suerte. Cada pantalla que viste es el resultado de lo que estudié, practiqué y reconstruí hasta que funcionó: la formación que transforma un archivo en blanco en un sistema, y un sistema en algo que vale la pena lanzar.", skills: { heading: "Habilidades y experiencia", description: "Estas son las herramientas, los lenguajes y los sistemas que uso en mi trabajo diario, junto con las experiencias que definieron mi manera de abordar cada proyecto.", }, contact: { heading: "Contactame", description: "Estoy abierto a trabajar en proyectos innovadores y ambiciosos que generen un impacto significativo.", }, theme: { light: "Cambiar al tema claro", dark: "Cambiar al tema oscuro", }, visuals: { aiLabel: "Claude Code, Codex y Cursor aportan a un mismo proceso de trabajo que produce un único resultado", education: [ "Universidad", "Flujos de apps", "Cambridge C1", "Hackatones", "Cursos", "Proyectos propios", "Ventas", "Atención al cliente", ], }, footer: "Diseñado y desarrollado de manera abierta.", componentShowcase: { versionLabel: "Versión del componente", preview: "Vista previa", usage: "Uso", dependencies: "Dependencias", dependencyDescription: (name: string) => `${name} importa estos archivos del proyecto. Copialos también si no tenés un equivalente.`, copy: "Copiar", copied: "Copiado", }, },} as const;
lib/cn.ts
/** Joins conditional class names. */export function cn( ...classes: Array<string | false | null | undefined>): string { return classes.filter(Boolean).join(" ");}