Floating Navbar
A compact floating navbar that sits anchored to the top-center of the area it
tracks. It reads scroll position with useScroll from motion-v and uses a
spring animation to slide up and down. By default it is fixed to the
viewport; pass a container to anchor it inside a scrollable element instead.
Usage
Pass a list of links, a brand name, and optionally toggle background blur. The
right slot lets you place actions on the opposite side of the nav.
Floating Navbar
Scroll down to see the navbar hide, scroll up to see it reappear.
<template>
<div
ref="box"
class="relative h-72 w-full max-w-lg overflow-y-auto rounded-2xl border border-default bg-muted/20"
>
<MFloatingNavbar
:container="box"
position="inline"
:links="links"
brand="Motion"
class="mx-auto mt-4 w-fit"
>
<template #right>
<UButton
size="xs"
label="Get Started"
/>
</template>
</MFloatingNavbar>
<div class="space-y-4 p-6">
<h3 class="text-lg font-semibold">
Floating Navbar
</h3>
<p class="text-muted text-sm">
Scroll down to see the navbar hide, scroll up to see it reappear.
</p>
<div
v-for="i in 8"
:key="i"
class="h-16 rounded-xl bg-default/80 shadow-sm"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const box = ref<HTMLElement | null>(null)
const links = [
{ label: 'Features', to: '#' },
{ label: 'Pricing', to: '#' },
{ label: 'Docs', to: '#' },
]
</script>
Examples
Solid background
With blur disabled the navbar uses an opaque surface instead of a translucent,
blurred one.
Solid background
With blur disabled the navbar uses an opaque surface instead of a translucent, blurred one.
<template>
<div
ref="box"
class="relative h-72 w-full max-w-md overflow-y-auto rounded-2xl border border-default bg-muted/20"
>
<MFloatingNavbar
:container="box"
position="inline"
:links="links"
brand="Motion"
:blur="false"
class="mx-auto mt-4 w-fit"
/>
<div class="space-y-4 p-6">
<h3 class="text-lg font-semibold">
Solid background
</h3>
<p class="text-muted text-sm">
With <code>blur</code> disabled the navbar uses an opaque surface instead of a
translucent, blurred one.
</p>
<div
v-for="i in 8"
:key="i"
class="h-16 rounded-xl bg-default/80 shadow-sm"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const box = ref<HTMLElement | null>(null)
const links = [
{ label: 'Features', to: '#' },
{ label: 'Pricing', to: '#' },
{ label: 'Docs', to: '#' },
]
</script>
Custom threshold
scrollThreshold controls how far you must scroll down before the navbar hides.
The default is 80px.
Custom threshold
With :scroll-threshold="240" the navbar stays visible until you have scrolled 240px down, then hides on the next downward scroll.
<template>
<div
ref="box"
class="relative h-72 w-full max-w-md overflow-y-auto rounded-2xl border border-default bg-muted/20"
>
<MFloatingNavbar
:container="box"
position="inline"
:links="links"
brand="Motion"
:scroll-threshold="240"
class="mx-auto mt-4 w-fit"
/>
<div class="space-y-4 p-6">
<h3 class="text-lg font-semibold">
Custom threshold
</h3>
<p class="text-muted text-sm">
With <code>:scroll-threshold="240"</code> the navbar stays visible until you
have scrolled 240px down, then hides on the next downward scroll.
</p>
<div
v-for="i in 8"
:key="i"
class="h-16 rounded-xl bg-default/80 shadow-sm"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const box = ref<HTMLElement | null>(null)
const links = [
{ label: 'Features', to: '#' },
{ label: 'Pricing', to: '#' },
{ label: 'Docs', to: '#' },
]
</script>
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
links | NavLink[] | [] | Array of navigation links, each with label and to used to render anchor
tags in the desktop nav. |
brand | string | '' | Text shown on the left side of the navbar. |
blur | boolean | true | If true, applies bg-default/70 backdrop-blur-xl; otherwise bg-default. |
container | HTMLElement | undefined | Scroll container to track. When provided, the navbar is sticky inside the
container and reacts to its scroll instead of the window's. |
position | 'fixed' | 'inline' | fixed | Positioning mode. Defaults to inline automatically when a container is
passed. fixed pins the navbar to the viewport; inline sticks it to the top
of its scroll container. |
hideOnScroll | boolean | true | When false the navbar stays visible regardless of scroll direction. |
scrollThreshold | number | 80 | Pixels of downward scroll required before the navbar hides. |
class | string | '' | Additional classes merged onto the root motion header. |
ui | object | - | Per-slot classes merged over the theme defaults. See UI. |
Slots
| Slot | Props | Description |
|---|---|---|
right | - | Content rendered on the right side of the navbar, aligned with the brand and links. |
UI
This component's theme exposes the slots below. Override any of them per instance with the ui prop, or app-wide under ui.floatingNavbar in app.config.ts.
| Prop | Type | Default | Description |
|---|---|---|---|
ui | object | - | Per-slot classes merged over the theme defaults. |
| Slot | Props | Description |
|---|---|---|
root | - | top-4 z-50 flex items-center gap-6 px-6 py-3 rounded-full border border-default shadow-lg |
brand | - | font-bold text-lg whitespace-nowrap |
nav | - | hidden md:flex items-center gap-1 |
link | - | px-3 py-1.5 rounded-full text-sm font-medium hover:bg-muted transition-colors |
Accessibility
The navbar is a <motion.header> element with an internal <nav> for the link
list. Links are standard anchors. Visibility is animated purely for presentation
and the element is never removed from the accessibility tree, so links remain
reachable with a keyboard. Consumers should ensure link text is meaningful.
Performance
- Tracks
scrollYfromuseScrolland reacts only onchangeviauseMotionValueEvent, avoiding unnecessary re-renders. - Compares current scroll to the last stored value and hides only after
scrolling down past
scrollThreshold, reducing flicker near the top. - Uses a spring transition on opacity and vertical position (centered via
x: '-50%'infixedmode) for smooth motion without layout thrash. - Classes are computed once per prop change; no timers created beyond the
scroll subscription managed by
motion-v.