feat(www): menu nav enhancements (#50282)

## What kind of change does this PR introduce?

feature reworks the www header dropdowns

## What is the current behavior?

menu dropdown navigation animation between items feels scattered

## What is the new behavior?

- adds dropdown card resize with a transition and the content crossfades
when switching
- removes dead zone between or under nav items
- sets card is centered on the screen + enhance tablet bp
- adds slight ui refresh spacing, colors, sizes

| state | preview |
| -------|------|
| before | <video
src="https://github.com/user-attachments/assets/acd8e161-a697-4070-b751-4f4e9f1eab19"
/> |
| after | <video
src="https://github.com/user-attachments/assets/fe403afd-0074-4cb5-9223-fd6cdd2f7d5a"
/> |



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Navigation dropdowns now provide smoother directional transitions,
keyboard-focus states, and reduced-motion support.
* Product navigation is organized into clearer Products and Modules
sections.
* Navigation layouts adapt earlier across screen sizes with responsive
two-column arrangements.

* **Style**
* Updated dropdown spacing, colors, borders, menu item styling, and
customer imagery sizing.
  * Refined blog loading placeholders with slightly tighter spacing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Anthony Lio authored and GitHub committed 2026-09-14 15:51:39 +03:00
1 parent 22d7bc0cfd
commit 819df2ae4d
6 files changed
+154 -119

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ function BlogListItemSkeleton() {
function BlogGridItemSkeleton() {
return (
<div className="flex flex-col gap-2 p-6">
<div className="flex flex-col gap-2 p-5">
<div className="relative w-full aspect-[1.91/1] overflow-hidden bg-foreground-muted/20 animate-pulse" />
<div className="flex items-center space-x-1.5 mt-2">
<div className="h-4 w-24 bg-foreground-muted/20 rounded animate-pulse" />
@@ -15,8 +15,8 @@ export const DevelopersDropdown = () => {
const { jobsCount, latestBlogPosts } = staticContent
return (
<div className="flex flex-col xl:flex-row">
<div className="w-[550px] xl:w-[470px] py-8 px-8 bg-background grid gap-3 grid-cols-2">
<div className="flex flex-col lg:flex-row">
<div className="w-full lg:w-[470px] px-6 py-5 bg-surface-75 grid gap-3 grid-cols-2">
{DevelopersData['navigation'].map((column) => (
<div key={column.label} className="p-0 flex flex-col gap-6">
<label className="text-foreground-lighter text-xs uppercase tracking-widest font-mono">
@@ -47,8 +47,8 @@ export const DevelopersDropdown = () => {
</div>
))}
</div>
<div className="bg-surface-75 flex flex-col w-[550px] xl:w-[480px] border-t xl:border-t-0 xl:border-l">
<div className="flex-col gap-2 py-8 px-10">
<div className="bg-surface-75 flex flex-col w-full lg:flex-1 border-t lg:border-t-0 lg:border-l">
<div className="flex-col gap-2 p-5">
<Link
href="/blog"
className="group flex items-center gap-1 text-foreground-lighter hover:text-foreground text-xs uppercase tracking-widest font-mono mb-5 focus-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background-alternative rounded-xs focus-visible:text-foreground"
+57 -91
View File
@@ -18,92 +18,60 @@ export const ProductDropdown = () => {
const isTablet = useBreakpoint(1279)
return (
<div className="flex flex-col xl:flex-row">
<div className="flex flex-col">
<div className="flex flex-row py-6 px-6 gap-2">
<ul className="flex flex-col gap-4 w-[280px] xl:w-[250px]">
{Object.values(MainProductsData)
.filter((product) => product.name !== 'Vector')
.map((product) => (
<NavigationMenuLink key={product.name} asChild>
<MenuItem
title={product.name}
href={product.url}
description={product.description_short}
icon={product.icon}
className="h-fit p-0"
hasChevron
/>
</NavigationMenuLink>
))}
</ul>
<div className="flex flex-col gap-4 w-[250px]">
<div className="flex flex-col lg:flex-row">
<div className="flex flex-col bg-surface-75">
<div className="flex flex-row p-6 pt-5 gap-6">
<div className="flex flex-col gap-4 w-[280px] lg:w-[250px]">
<div className="flex items-center gap-1 text-foreground-lighter text-xs uppercase tracking-widest font-mono">
Features
</div>
<ul className="flex flex-col gap-4">
{Object.values(MainProductsData)
.filter((product) => product.name !== 'Vector')
.map((product) => (
<NavigationMenuLink key={product.name} asChild>
<MenuItem
title={product.name}
href={product.url}
description={product.description_short}
icon={product.icon}
className="h-fit p-0"
hasChevron
/>
</NavigationMenuLink>
))}
</ul>
</div>
<div className="flex flex-col gap-4 w-72">
<div className="group flex items-center gap-1 text-foreground-lighter text-xs uppercase tracking-widest font-mono">
Modules
</div>
<ul className="flex flex-col gap-4">
{Object.values(ProductModulesData).map((productModule) => (
<NavigationMenuLink key={productModule.name} asChild>
<Link
<MenuItem
title={productModule.name}
href={productModule.url}
className="
h-fit group/menu-item
flex items-start gap-2
text-xs leading-none
text-foreground-light hover:text-foreground
no-underline select-none
focus-ring focus-visible:text-foreground
"
>
<div className="w-8 h-8 min-w-8 shrink-0 bg-background border flex items-center justify-center rounded-md">
<svg
className="h-4 w-4 group-hover/menu-item:text-foreground group-focus-visible/menu-item:text-foreground"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d={productModule.icon}
stroke="currentColor"
/>
</svg>
</div>
<div className="flex flex-col justify-center">
<div className="flex items-center gap-1">
<p className="leading-snug text-foreground">{productModule.name}</p>
<ChevronRight
strokeWidth={2}
className="w-3 h-3 text-foreground transition-all will-change-transform -translate-x-1 opacity-0 group-hover/menu-item:translate-x-0 group-hover/menu-item:opacity-100"
/>
</div>
{productModule.description_short && (
<p className="line-clamp-2 leading-tight text-foreground-lighter group-hover/menu-item:text-foreground-light group-focus-visible/menu-item:text-foreground-light">
{productModule.description_short}
</p>
)}
</div>
</Link>
description={productModule.description_short}
icon={productModule.icon}
className="h-fit p-0"
hasChevron
/>
</NavigationMenuLink>
))}
</ul>
<div className="pt-4 border-t">
<Link
href="/features"
className="
h-fit group/menu-item
flex items-start gap-2
text-xs leading-none
flex items-center gap-3
text-sm leading-none
text-foreground-light hover:text-foreground
no-underline select-none
focus-ring focus-visible:text-foreground
"
>
<div className="w-8 h-8 min-w-8 shrink-0 bg-background border flex items-center justify-center rounded-md">
<Sparkles size={16} strokeWidth={1.3} />
<div className="w-10 h-10 min-w-10 shrink-0 bg-surface-200 border flex items-center justify-center rounded-lg">
<Sparkles size={20} strokeWidth={1.5} />
</div>
<div className="flex flex-col justify-center">
<div className="flex items-center gap-1">
@@ -113,17 +81,17 @@ export const ProductDropdown = () => {
className="w-3 h-3 text-foreground transition-all will-change-transform -translate-x-1 opacity-0 group-hover/menu-item:translate-x-0 group-hover/menu-item:opacity-100"
/>
</div>
<span className="line-clamp-2 leading-tight text-foreground-lighter group-hover/menu-item:text-foreground-light group-focus-visible/menu-item:text-foreground-light">
<span className="line-clamp-2 leading-snug text-xs text-foreground-lighter group-hover/menu-item:text-foreground-light group-focus-visible/menu-item:text-foreground-light">
Explore everything you can do with Supabase.
</span>
</div>
</Link>
</div>
</ul>
</div>
</div>
</div>
<div className="bg-surface-75 border-t xl:border-t-0 xl:border-l p-6 gap-8 flex flex-col w-full xl:w-[400px]">
<div className="col-span-3 flex flex-row gap-8 xl:w-auto">
<div className="bg-surface-75 border-t lg:border-t-0 lg:border-l p-6 gap-8 flex flex-col w-full lg:flex-1">
<div className="col-span-3 flex flex-row gap-8 lg:w-auto">
<div>
<Link
href="/customers"
@@ -139,7 +107,7 @@ export const ProductDropdown = () => {
href={customer.url}
className="group flex items-center gap-3 focus-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background-alternative rounded-sm"
>
<div className="relative rounded-md bg-background border group-hover:border-foreground-muted/50 h-14 w-28 xl:h-14 xl:w-20 shrink-0 overflow-auto">
<div className="relative rounded-md bg-background border group-hover:border-foreground-muted/50 h-14 w-28 lg:h-14 lg:w-20 shrink-0 overflow-auto">
<Image
src={`/${customer.imgUrl}`}
alt={customer.title}
@@ -158,24 +126,22 @@ export const ProductDropdown = () => {
</ul>
</div>
</div>
<div className="w-full col-span-2 grid grid-cols-4 xl:grid-cols-5 gap-6 xl:gap-2">
<div className="col-span-2 xl:col-span-3">
<p className="text-foreground-lighter text-xs uppercase tracking-widest font-mono mb-3">
{ComparisonsData.label}
</p>
<ul className="flex flex-col gap-2">
{ComparisonsData.comparisons.map((link) => (
<li key={link.text}>
<TextLink
chevronAnimation="fadeIn"
url={link.url}
label={link.text}
className="mt-0 hover:text-foreground focus-visible:text-foreground focus-visible:ring-offset-4 focus-visible:ring-offset-background-overlay"
/>
</li>
))}
</ul>
</div>
<div className="w-full">
<p className="text-foreground-lighter text-xs uppercase tracking-widest font-mono mb-3">
{ComparisonsData.label}
</p>
<ul className="flex flex-col gap-2">
{ComparisonsData.comparisons.map((link) => (
<li key={link.text}>
<TextLink
chevronAnimation="fadeIn"
url={link.url}
label={link.text}
className="mt-0 hover:text-foreground focus-visible:text-foreground focus-visible:ring-offset-4 focus-visible:ring-offset-background-overlay"
/>
</li>
))}
</ul>
</div>
</div>
</div>
+27 -15
View File
@@ -1,9 +1,10 @@
import { useBreakpoint, useReducedMotion } from 'common'
import { navData as DevelopersData } from 'data/Solutions'
import { AnimatePresence, motion } from 'framer-motion'
import { ArrowLeftRight, ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useWindowSize } from 'react-use'
import { useMedia } from 'react-use'
type LinkProps = {
text: string
@@ -14,16 +15,16 @@ type LinkProps = {
}
export const SolutionsDropdown = () => (
<div className="flex flex-col xl:flex-row">
<div className="w-[550px] xl:w-[600px] py-8 px-8 bg-background grid gap-3 grid-cols-2 xl:grid-cols-3">
<div className="bg-surface-75 flex flex-col lg:flex-row">
<div className="w-full lg:w-[600px] px-6 py-5 grid gap-3 grid-cols-2 lg:grid-cols-3">
{/* Skill Level, Who it's for, App Type */}
{DevelopersData['navigation'].slice(0, 3).map((column) => (
<LinksGroup key={column.label} links={column.links} label={column.label} />
))}
</div>
<div className="bg-surface-75 flex flex-col w-[550px] xl:w-[480px] border-t xl:border-t-0 xl:border-l">
<div className="flex flex-col gap-6 py-8 px-10">
<div className="flex flex-col w-full lg:flex-1 border-t lg:border-t-0 lg:border-l">
<div className="flex flex-col gap-6 p-5">
<label className="text-foreground-lighter text-xs uppercase tracking-widest font-mono">
{DevelopersData['navigation'][3].label}
</label>
@@ -68,26 +69,34 @@ const LinksGroup = ({ links, label }: { links: LinkProps[]; label: string }) =>
const MotionLink = motion(Link)
const MigrationLinkCard = ({ link }: { link: LinkProps }) => {
const [hovered, setHovered] = useState(false)
const { width } = useWindowSize()
const isDesktop = width >= 1280
const [isHighlighted, setIsHighlighted] = useState(false)
const canHover = useMedia('(hover: hover)', true)
const reduceMotion = useReducedMotion()
const isDesktop = !useBreakpoint('lg')
const duration = isDesktop && !reduceMotion ? 0.2 : 0
const isIconVisible = isHighlighted || !isDesktop || !canHover
return (
<MotionLink
href={link.url!}
className="bg-background p-3 rounded-md border flex items-center gap-4 group overflow-hidden"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onMouseEnter={() => setIsHighlighted(true)}
onMouseLeave={() => setIsHighlighted(false)}
onFocus={() => setIsHighlighted(true)}
onBlur={() => setIsHighlighted(false)}
layout
layoutDependency={isHighlighted}
transition={{ duration, ease: 'easeInOut' }}
>
<AnimatePresence mode="popLayout">
{(hovered || !isDesktop) && (
{isIconVisible && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -10 }}
transition={{ duration: isDesktop ? 0.2 : 0, ease: 'easeInOut' }}
transition={{ duration, ease: 'easeInOut' }}
layout
layoutDependency={isHighlighted}
>
{link.icon && <link.icon className="size-6" />}
</motion.div>
@@ -97,7 +106,8 @@ const MigrationLinkCard = ({ link }: { link: LinkProps }) => {
<motion.div
className="size-6 flex items-center justify-center"
layout
transition={{ duration: isDesktop ? 0.2 : 0, ease: 'easeInOut' }}
layoutDependency={isHighlighted}
transition={{ duration, ease: 'easeInOut' }}
>
<ArrowLeftRight className="size-4 text-foreground-light" strokeWidth={1.3} />
</motion.div>
@@ -105,14 +115,16 @@ const MigrationLinkCard = ({ link }: { link: LinkProps }) => {
<motion.div
className="flex items-center gap-1"
layout
transition={{ duration: isDesktop ? 0.2 : 0, ease: 'easeInOut' }}
layoutDependency={isHighlighted}
transition={{ duration, ease: 'easeInOut' }}
>
<span className="text-base font-medium">{link.text}</span>
</motion.div>
<motion.div
layout
transition={{ duration: isDesktop ? 0.2 : 0, ease: 'easeInOut' }}
layoutDependency={isHighlighted}
transition={{ duration, ease: 'easeInOut' }}
className="ml-auto"
>
<ChevronRight
+64 -7
View File
@@ -19,6 +19,7 @@ import {
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
NavigationMenuViewport,
} from 'ui'
import { AuthenticatedDropdownMenu } from 'ui-patterns/AuthenticatedDropdownMenu'
import { AnnouncementBanner } from 'ui-patterns/Banners/AnnouncementBanner'
@@ -39,10 +40,29 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
const pathname = usePathname()
const { width } = useWindowSize()
const [open, setOpen] = useState(false)
const [activeDropdown, setActiveDropdown] = useState('')
const [visibleDropdown, setVisibleDropdown] = useState('')
const [isSwitchingDropdown, setIsSwitchingDropdown] = useState(false)
const [openedDropdowns, setOpenedDropdowns] = useState<string[]>([])
const handleDropdownChange = (value: string) => {
// animate card height between two open menus only
setIsSwitchingDropdown(value !== '' && activeDropdown !== '')
setActiveDropdown(value)
if (value === '') return
setVisibleDropdown(value)
if (!openedDropdowns.includes(value)) setOpenedDropdowns([...openedDropdowns, value])
}
const isLoggedIn = useIsLoggedIn()
const isUserLoading = useIsUserLoading()
const user = useUser()
const menu = getMenu()
const dropdownTitles: string[] = menu.primaryNav
.filter((menuItem) => menuItem.hasDropdown)
.map((menuItem) => menuItem.title)
const getDropdownSide = (title: string) => {
if (title === visibleDropdown) return 'active'
return dropdownTitles.indexOf(title) < dropdownTitles.indexOf(visibleDropdown) ? 'start' : 'end'
}
const sendTelemetryEvent = useSendTelemetryEvent()
const userMenu = useDropdownMenu(user)
@@ -116,22 +136,43 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
</div>
<NavigationMenu
delayDuration={0}
className="hidden pl-8 sm:space-x-4 lg:flex h-16"
viewportClassName="rounded-xl bg-background"
value={activeDropdown}
onValueChange={handleDropdownChange}
renderViewport={false}
className="static hidden pl-8 lg:flex h-16 items-stretch"
>
<NavigationMenuList>
<NavigationMenuList className="h-full space-x-0 items-stretch">
{menu.primaryNav.map((menuItem) =>
menuItem.hasDropdown ? (
<NavigationMenuItem className="text-sm font-medium" key={menuItem.title}>
<NavigationMenuItem
className="text-sm font-medium"
key={menuItem.title}
value={menuItem.title}
>
<NavigationMenuTrigger
className={cn(
buttonVariants({ variant: 'text', size: 'small' }),
'bg-transparent! hover:text-brand-link data-open:text-brand-link! focus-ring focus-visible:text-foreground px-2 h-auto'
'bg-transparent! hover:text-brand-link data-open:text-brand-link! focus-ring focus-visible:text-foreground px-2.5 h-full'
)}
>
{menuItem.title}
</NavigationMenuTrigger>
<NavigationMenuContent>{menuItem.dropdown}</NavigationMenuContent>
<NavigationMenuContent
forceMount
inert={visibleDropdown !== menuItem.title}
data-active={visibleDropdown === menuItem.title}
data-side={getDropdownSide(menuItem.title)}
className={cn(
'md:w-full data-[motion^=from-]:animate-none! data-[motion^=to-]:animate-none!',
'data-[active=false]:pointer-events-none data-[active=false]:opacity-0',
'data-[side=start]:-translate-x-4 data-[side=end]:translate-x-4',
'motion-safe:group-data-[switching=true]/viewport:transition-[opacity,translate]',
'motion-safe:group-data-[switching=true]/viewport:duration-250',
'motion-safe:group-data-[switching=true]/viewport:ease-in-out'
)}
>
{openedDropdowns.includes(menuItem.title) ? menuItem.dropdown : null}
</NavigationMenuContent>
</NavigationMenuItem>
) : (
<NavigationMenuItem className="text-sm font-medium" key={menuItem.title}>
@@ -139,7 +180,7 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
<MenuItem
href={menuItem.url}
title={menuItem.title}
className="group-hover:bg-transparent text-foreground focus-visible:text-brand-link"
className="group-hover:bg-transparent text-foreground focus-visible:text-brand-link px-2.5 h-full"
hoverColor="brand"
/>
</NavigationMenuLink>
@@ -147,6 +188,22 @@ const Nav = ({ hideNavbar, stickyNavbar = true }: Props) => {
)
)}
</NavigationMenuList>
<NavigationMenuViewport
forceMount
data-open={activeDropdown !== ''}
data-switching={isSwitchingDropdown}
containerProps={{ className: 'inset-x-0' }}
className={cn(
'group/viewport origin-top scale-100 rounded-xl bg-surface-75 md:w-[960px]',
'data-[state=open]:animate-none! data-[state=closed]:animate-none!',
'data-[state=open]:duration-200 data-[state=open]:ease-out data-[state=closed]:duration-200',
'data-[open=false]:invisible data-[open=false]:scale-[0.97] data-[open=false]:opacity-0',
'data-[open=false]:pointer-events-none',
'motion-safe:transition-[opacity,scale,visibility]',
'motion-safe:data-[switching=true]:transition-[height,opacity,scale,visibility]',
'motion-reduce:transition-none'
)}
/>
</NavigationMenu>
</div>
<div className="flex items-center gap-2 opacity-0 animate-fade-in scale-100! delay-300">
@@ -1,8 +1,8 @@
'use client'
import { NavigationMenu as NavigationMenuPrimitive } from 'radix-ui'
import { cva } from 'class-variance-authority'
import { ChevronDown } from 'lucide-react'
import { NavigationMenu as NavigationMenuPrimitive } from 'radix-ui'
import * as React from 'react'
import { cn } from '../../../lib/utils/cn'