Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions apps/docs/content/2.block/navbar-gooey-bloom.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
title: Navbar Gooey Bloom
description: A navbar whose hover highlight is a pool of lime that stretches between links, with menus that swell out of a drop.
icon: blend
---

A glass navbar with **liquid for a highlight**. Point at a link and a lime pool
**stretches across the bar**: its front edge runs ahead, its back edge drags a
droplet behind it, and the label **inverts to ink** as the pool arrives. Every
menu is a **gooey panel** that swells out of a small drop under the trigger,
keeps a soft bulge pointed at it, and **leans and stretches** when you move to
the next menu. Inside, a lime blob **flows from icon to icon** as you move down
the list. The bar wears the Tween UI mark and uses the Dot Bloom Button for its action. GSAP, a
small spring loop and three SVG filters.

::component-preview{name="navbar-gooey-bloom"}
::

## Installation

::tabs
:::tabs-item{label="CLI" icon="i-lucide-terminal"}

::install-tabs{item="navbar-gooey-bloom"}
::

:::
:::tabs-item{label="Manual" icon="i-lucide-clipboard"}

::component-source{name="navbar-gooey-bloom"}
::

:::
::

## Usage

::tabs
:::tabs-item{label="React" icon="i-lucide-atom"}

```tsx [app/page.tsx]
import NavbarGooeyBloom from '@/components/tweenui/navbar-gooey-bloom';

export default function Page() {
return <NavbarGooeyBloom />;
}
```

:::
::

It arrives dressed as a Tween UI demo, with three mega menus, one plain link
and a single hero as the page. Put your own page inside it as `children`:

```tsx [app/layout.tsx]
'use client';

import { useRouter } from 'next/navigation';
import NavbarGooeyBloom from '@/components/tweenui/navbar-gooey-bloom';

export default function Layout({ children }: { children: React.ReactNode }) {
const router = useRouter();

return (
<NavbarGooeyBloom
brand="Kairo"
currentHref="/pricing"
actionLabel="Start free"
onActionClick={() => router.push('/signup')}
menus={[
{
label: 'Product',
columns: [
[
{
label: 'Overview',
href: '/product',
description: 'What it does, in one screen.',
icon: 'bloom',
},
{
label: 'Pricing',
href: '/pricing',
description: 'Three plans, no surprises.',
icon: 'layers',
},
],
],
feature: {
title: 'What is new',
description: 'The changelog, kept short.',
href: '/changelog',
cta: 'Read it',
},
},
{ label: 'Docs', href: '/docs' },
]}
>
{children}
</NavbarGooeyBloom>
);
}
```

A menu is `{ label, columns?, feature?, href? }`. Give it `columns` (an array
of columns, each an array of links) to make it a panel; give it only `href` to
make it a plain link. A link is `{ label, href, description?, icon? }`, with
`icon` one of `'bloom'`, `'drop'`, `'orbit'`, `'spark'`, `'layers'`, `'wave'`,
`'bolt'` or `'shield'`. A feature is `{ title, description, href, cta?, image? }`;
without an image it draws its own gooey bloom mark.

## Props

| Prop | Type | Default | Description |
| --------------- | -------------------------------------- | ----------------- | ---------------------------------------------- |
| `brand` | `string` | `'Tween UI'` | Name beside the logo, and the section's label. |
| `logo` | `ReactNode` | The Tween UI mark | Shown before the brand name. |
| `homeHref` | `string` | `'/'` | Where the logo links. |
| `menus` | `NavbarGooeyBloomMenu[]` | Three + Journal | Bar links, in order, with their panels. |
| `currentHref` | `string` | `'/'` | The page you are on; nested paths count too. |
| `actionLabel` | `string` | `'Get started'` | Label of the Dot Bloom button in the bar. |
| `onActionClick` | `MouseEventHandler<HTMLButtonElement>` | — | Runs when the action button is pressed. |
| `children` | `ReactNode` | A sample hero | The page under the bar. |

## Behavior

- **The pool runs on springs, not tweens.** Each edge of the highlight is a
critically damped spring. The edge facing the direction of travel is pulled
harder than the one behind it, so the pool stretches and thins while it moves
and gathers back under the label when it stops. Retargeting keeps the
current velocity, so sweeping across the bar never jerks.
- **One droplet, never loose.** A single drop sits on the pool's centre line
inside a `feGaussianBlur` + `feColorMatrix` layer. It lags the pool on a
softer spring but is clamped to its body, so it shows as a tail on the
trailing end and is absorbed when the pool settles.
- **Menus swell out of a drop.** The panel starts as a small blob under the
trigger and springs out to its full width and height. A goo filter merges the
panel with a bulge on its top edge that follows the pool, so the panel always
leans toward its trigger. Its edges also lead and trail when you switch
menus, so the panel stretches across before it settles.
- **Contents are cut to the liquid.** The rows are clipped to the panel's
current shape every frame, so they are revealed as it grows. Items rise
10px in, 20ms apart.
- **Rows share one lime blob.** A single blob behind the icons follows your
pointer from row to row with the same leading and trailing springs, thinning
as it stretches and dragging a small drop. A soft surface tracks the row, the
icon inverts to ink and a lime arrow slides in beside the label.
- **The sample fits the block.** The hero uses `container-type: size` and
`cqh` type steps, so it fills the stage at any height without an inner
scrollbar. On pages taller than the block, the bar hides after 220px of
downward travel and returns on the way up, and never hides while a panel is
open. Scroll position is read from the block's own scroller.
- **Below 1024px** the links fold into a full-screen sheet with accordion
menus, the same Dot Bloom action and the same icons as the panels.

## Accessibility

::note
Triggers are real buttons with `aria-expanded`, `aria-controls` and
`aria-haspopup`, and the panel is `inert` while closed, so the 20-odd links
inside it never enter the tab order unseen. Focus opens a menu and moves the
row blob, the header's `focusout` closes it when focus leaves, Escape closes it
and returns focus to the trigger. The mobile sheet is `inert` when closed,
traps its own scroll, closes on Escape and hands focus back to the toggle.
Labels keep contrast because the pool inverts text to ink (`#03110f` on
`#c6f56f`). Under `prefers-reduced-motion: reduce` the springs settle at once,
the intro and sheet animations are skipped, and every menu still opens and
closes.
::
22 changes: 22 additions & 0 deletions apps/docs/public/r/navbar-gooey-bloom-demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "navbar-gooey-bloom-demo",
"type": "registry:example",
"title": "Navbar Gooey Bloom Demo",
"description": "Usage examples for Navbar Gooey Bloom.",
"registryDependencies": [
"https://tween-ui.vercel.app/r/navbar-gooey-bloom.json"
],
"files": [
{
"path": "examples/navbar-gooey-bloom-demo.tsx",
"type": "registry:example",
"content": "import NavbarGooeyBloom from '@/components/tweenui/navbar-gooey-bloom';\n\nexport default function Page() {\n return <NavbarGooeyBloom />;\n}"
},
{
"path": "examples/navbar-gooey-bloom-demo-2.tsx",
"type": "registry:example",
"content": "'use client';\n\nimport { useRouter } from 'next/navigation';\nimport NavbarGooeyBloom from '@/components/tweenui/navbar-gooey-bloom';\n\nexport default function Layout({ children }: { children: React.ReactNode }) {\n const router = useRouter();\n\n return (\n <NavbarGooeyBloom\n brand=\"Kairo\"\n currentHref=\"/pricing\"\n actionLabel=\"Start free\"\n onActionClick={() => router.push('/signup')}\n menus={[\n {\n label: 'Product',\n columns: [\n [\n {\n label: 'Overview',\n href: '/product',\n description: 'What it does, in one screen.',\n icon: 'bloom',\n },\n {\n label: 'Pricing',\n href: '/pricing',\n description: 'Three plans, no surprises.',\n icon: 'layers',\n },\n ],\n ],\n feature: {\n title: 'What is new',\n description: 'The changelog, kept short.',\n href: '/changelog',\n cta: 'Read it',\n },\n },\n { label: 'Docs', href: '/docs' },\n ]}\n >\n {children}\n </NavbarGooeyBloom>\n );\n}"
}
]
}
26 changes: 26 additions & 0 deletions apps/docs/public/r/navbar-gooey-bloom.json

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions apps/docs/public/r/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,34 @@
"type": "registry:example"
}
]
},
{
"name": "navbar-gooey-bloom",
"type": "registry:block",
"title": "Navbar Gooey Bloom",
"description": "A glass navbar whose hover highlight is liquid: a lime pool stretches from link to link with a droplet dragging behind, while the label inverts to ink. Menus swell out of a drop into one gooey panel that leans toward its trigger and morphs between layouts, and inside it a lime blob flows from icon to icon.",
"files": [
{
"path": "components/tweenui/navbar-gooey-bloom.tsx",
"type": "registry:component"
}
]
},
{
"name": "navbar-gooey-bloom-demo",
"type": "registry:example",
"title": "Navbar Gooey Bloom Demo",
"description": "Usage examples for Navbar Gooey Bloom.",
"files": [
{
"path": "examples/navbar-gooey-bloom-demo.tsx",
"type": "registry:example"
},
{
"path": "examples/navbar-gooey-bloom-demo-2.tsx",
"type": "registry:example"
}
]
}
]
}
1 change: 1 addition & 0 deletions apps/docs/registry/__index__.generated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const demos: Record<string, ComponentType> = {
'navbar-corner-panel:default': dynamic(() => import('./demos/navbar-corner-panel')),
'footer-pulse-links:default': dynamic(() => import('./demos/footer-pulse-links')),
'arc-carousel:default': dynamic(() => import('./demos/arc-carousel')),
'navbar-gooey-bloom:default': dynamic(() => import('./demos/navbar-gooey-bloom')),
};

export function getDemo(component: string, variant: string): ComponentType | undefined {
Expand Down
12 changes: 12 additions & 0 deletions apps/docs/registry/__sources__.generated.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/docs/registry/demos/navbar-corner-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import NavbarCornerPanel from '@/registry/tweenui/navbar-corner-panel';

const NavbarCornerPanelDemo = () => {
return (
<div className="w-full overflow-hidden rounded-xl">
<div className="w-full overflow-hidden rounded-xl [[data-block-stage]_&]:rounded-none">
<NavbarCornerPanel className="h-[600px] [[data-block-stage]_&]:h-svh" />
</div>
);
Expand Down
13 changes: 13 additions & 0 deletions apps/docs/registry/demos/navbar-gooey-bloom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

import NavbarGooeyBloom from '@/registry/tweenui/navbar-gooey-bloom';

const NavbarGooeyBloomDemo = () => {
return (
<div className="w-full overflow-hidden rounded-xl [[data-block-stage]_&]:rounded-none">
<NavbarGooeyBloom className="h-[600px] [[data-block-stage]_&]:h-svh" />
</div>
);
};

export default NavbarGooeyBloomDemo;
2 changes: 1 addition & 1 deletion apps/docs/registry/demos/navbar-pill-glide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import NavbarPillGlide from '@/registry/tweenui/navbar-pill-glide';

const NavbarPillGlideDemo = () => {
return (
<div className="w-full overflow-hidden rounded-xl">
<div className="w-full overflow-hidden rounded-xl [[data-block-stage]_&]:rounded-none">
<NavbarPillGlide className="h-[600px] [[data-block-stage]_&]:h-svh" />
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/registry/demos/navbar-push-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import NavbarPushMenu from '@/registry/tweenui/navbar-push-menu';

const NavbarPushMenuDemo = () => {
return (
<div className="w-full overflow-hidden rounded-xl">
<div className="w-full overflow-hidden rounded-xl [[data-block-stage]_&]:rounded-none">
<NavbarPushMenu className="h-[600px] [[data-block-stage]_&]:h-svh" />
</div>
);
Expand Down
34 changes: 34 additions & 0 deletions apps/docs/registry/registry-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1146,4 +1146,38 @@ export default function Example() {
},
docs: 'Pass your own `items`; eight or more keep the arc full and the wrap unseen. Colors are inline Tailwind classes, so there is nothing to add to your theme.',
},
{
name: 'navbar-gooey-bloom',
type: 'block',
addedAt: '2026-10-20',
title: 'Navbar Gooey Bloom',
description:
'A glass navbar whose hover highlight is liquid: a lime pool stretches from link to link with a droplet dragging behind, while the label inverts to ink. Menus swell out of a drop into one gooey panel that leans toward its trigger and morphs between layouts, and inside it a lime blob flows from icon to icon.',
media: {},
dependencies: ['gsap', '@gsap/react'],
registryDependencies: ['dot-bloom-button'],
cssVars: {},
files: [
{
path: 'registry/tweenui/navbar-gooey-bloom.tsx',
target: 'components/tweenui/navbar-gooey-bloom.tsx',
kind: 'react',
},
],
variants: [
{
id: 'default',
label: 'Preview',
reactSource: 'registry/tweenui/navbar-gooey-bloom.tsx',
},
],
usage: {
react: `import NavbarGooeyBloom from '@/components/tweenui/navbar-gooey-bloom';

export default function Example() {
return <NavbarGooeyBloom />;
}`,
},
docs: 'Pass `menus` (a `label` plus `columns` and an optional `feature`, or just an `href` for a plain link) and put your page inside as `children`. The goo filter is generated per instance, so several bars can share a page.',
},
];
Loading
Loading