Lift Button
A floating scroll-to-top button that gracefully appears on scroll, with support for custom containers, target-based navigation, and fully customizable styling.
Top of the content
Scroll down to trigger the floating lift button.
Section 1
This is a sample content block to demonstrate the scroll behavior.
Section 2
This is a sample content block to demonstrate the scroll behavior.
Section 3
This is a sample content block to demonstrate the scroll behavior.
Section 4
This is a sample content block to demonstrate the scroll behavior.
Section 5
This is a sample content block to demonstrate the scroll behavior.
Section 6
This is a sample content block to demonstrate the scroll behavior.
Section 7
This is a sample content block to demonstrate the scroll behavior.
Section 8
This is a sample content block to demonstrate the scroll behavior.
Installation
Run the following command to add the lift-button component to your project using the Flow UI registry:
npx shadcn@latest add @flowui/lift-buttonInstall Dependencies
Ensure you have lucide-react installed for the default icon.
npm install lucide-reactCreate Lift Button Component
Create a new file lift-button.tsx in your components folder (e.g., components/ui/lift-button.tsx) and copy the code below.
"use client";
import { cn } from "@/lib/utils";
import { ArrowUp } from "lucide-react";
import { ComponentProps, useEffect, useState } from "react";
type LiftButtonProps = {
className?: string;
children?: React.ReactNode;
scrollContainerId?: string;
targetId?: string;
alwaysVisible?: boolean;
threshold?: number;
} & ComponentProps<"button">;
const LiftButton = ({
className,
children,
scrollContainerId,
targetId,
alwaysVisible = false,
threshold = 0,
...props
}: LiftButtonProps) => {
// State to control the visibility
const [visible, setVisible] = useState<boolean>(alwaysVisible);
// Scroll based button visibility
useEffect(() => {
if (alwaysVisible) return;
// Get the scroll container
const container = scrollContainerId ? document.getElementById(scrollContainerId) : window;
if (!container) return;
const handleScroll = () => {
const scrollPosition = container === window ?
window.pageYOffset || document.documentElement.scrollTop :
(container as HTMLElement).scrollTop;
setVisible(scrollPosition > threshold);
};
container?.addEventListener("scroll", handleScroll, { passive: true });
handleScroll();
return () => container?.removeEventListener("scroll", handleScroll);
}, [alwaysVisible, threshold, scrollContainerId]);
// Scroll to top functionality
const handleClick = () => {
if(scrollContainerId) {
const container = document.getElementById(scrollContainerId);
if(!container) return;
if(targetId) {
const target = document.getElementById(targetId);
if(!target) return;
const containerRec = container.getBoundingClientRect();
const targetRec = target.getBoundingClientRect();
const offset = targetRec.top - containerRec.top + container.scrollTop;
container.scrollTo({ top: offset, behavior: "smooth" });
} else {
container.scrollTo({ top: 0, behavior: "smooth" });
}
return;
}
if(targetId) {
const target = document.getElementById(targetId);
target?.scrollIntoView({ behavior: "smooth" });
return;
} else {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
return (
<button
className={cn(
"inline-flex justify-center items-center transition-all duration-300",
className,
visible ? "opacity-100" : "opacity-0 pointer-events-none"
)}
{...props}
onClick={handleClick}
>
{children ? (
children
) : (
<ArrowUp className="size-5" />
)}
</button>
)
}
export default LiftButton;Usage
Always Visible
Set the alwaysVisible prop to keep the button permanently visible without requiring any scroll.
Always Visible
Perfect for static layouts
import LiftButton from "@/components/ui/lift-button";
import { ArrowUp } from "lucide-react";
export function AlwaysVisibleExample() {
return (
<LiftButton
alwaysVisible
className="size-9 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90"
>
<ArrowUp className="size-4" />
</LiftButton>
);
}Scroll to a Specific Section
Use the targetId prop to scroll to a specific DOM element instead of the top of the page/container. Combine with scrollContainerId to scope scrolling within a container.
This is the top section.
Target Section
Will scroll back here
Click the rocket below
import LiftButton from "@/components/ui/lift-button";
export const ScrollToSectionExample = () => {
return (
<div className="relative h-75">
{/* Scrollable area */}
<div id="my-container" className="h-full overflow-y-auto pr-12">
<div id="target-section">
{/* Content */}
</div>
</div>
{/* Button */}
<LiftButton
scrollContainerId="my-container"
targetId="target-section"
>
{/* Icon */}
</LiftButton>
</div>
);
}Custom Threshold
Control how far the user must scroll before the button appears by setting the threshold prop (in pixels).
Threshold: 200px
import LiftButton from "@/components/ui/lift-button";
// Button appears only after scrolling 200px
<LiftButton threshold={200} className="...">
<ArrowUp className="size-4" />
</LiftButton>Understand the positioning
import LiftButton from "@/components/ui/lift-button";
export const ScrollToSectionExample = () => {
return (
// Outer Container - Help to position the button, if using absolute styling
<div className="relative h-75">
{/* Main Scrollable Container + Target Element */}
<div id="my-container" className="w-full h-full overflow-y-auto">
<div id="target-section"></div>
</div>
{/* Button - Inside the outer container */}
<LiftButton scrollContainerId="my-container" targetId="target-section">
</LiftButton>
</div>
);
}Positioning
The LiftButton does not apply any positioning by default.
You must provide positioning styles such as fixed, absolute, or sticky via className depending on your layout needs.
Props
The LiftButton component accepts all native <button> HTML attributes, plus the following:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for styling the button. |
children | React.ReactNode | <ArrowUp /> | Custom content inside the button. Defaults to an ArrowUp icon from lucide-react. |
scrollContainerId | string | - | The id of a scrollable container to listen to. Defaults to window if not provided. |
targetId | string | - | The id of a target element to scroll into view. If not set, scrolls to the top of the container. |
alwaysVisible | boolean | false | When true, the button is permanently visible regardless of scroll position. |
threshold | number | 0 | The scroll distance (in pixels) the user must exceed before the button becomes visible. |
Tip
Since the component extends the native <button> element, you can pass any standard button attributes like aria-label, disabled, title, etc. for full accessibility control.