Scroll aware fab
FreeThe main action kept in reach: it shows its label at the top and when scrolling up, and shrinks to its icon while you scroll down.
pnpm dlx shadcn@latest add https://caliperui.site/r/scroll-aware-fab.json- Amara Okaforapproved INV-2041
- Tomás Ferreiracommented on the Ostra deal
- Priya Ramanmatched 12 payments
- Kiara Murayamainvited a teammate
- Rafael Abreuclosed September
- Amara Okaforexported the ledger
- Tomás Ferreirachanged a rule
- Priya Ramanapproved INV-2041
- Kiara Murayamacommented on the Ostra deal
- Rafael Abreumatched 12 payments
- Amara Okaforinvited a teammate
- Tomás Ferreiraclosed September
- Priya Ramanexported the ledger
- Kiara Murayamachanged a rule
- Rafael Abreuapproved INV-2041
- Amara Okaforcommented on the Ostra deal
- Tomás Ferreiramatched 12 payments
- Priya Ramaninvited a teammate
- Kiara Murayamaclosed September
- Rafael Abreuexported the ledger
- Amara Okaforchanged a rule
- Tomás Ferreiraapproved INV-2041
- Priya Ramancommented on the Ostra deal
- Kiara Murayamamatched 12 payments
When to use #
Use it for
- Phone screens with one primary action, like compose.
- Long lists where the action should not cover content.
Skip it when
- Desktop layouts; put the action in the header.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/scroll-aware-fab.json
Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { PenLine } from "lucide-react" import { useRef } from "react" import { Avatar } from "@/components/ui/avatar" import { ScrollAwareFab } from "@/components/ui/scroll-aware-fab" const actions = ["approved INV-2041", "commented on the Ostra deal", "matched 12 payments", "invited a teammate", "closed September", "exported the ledger", "changed a rule"]; const names = ["Amara Okafor", "Tomás Ferreira", "Priya Raman", "Kiara Murayama", "Rafael Abreu"]; export function ScrollAwareFabDemo() { const scroller = useRef<HTMLDivElement>(null); return ( <div className="relative h-[440px] w-full max-w-[360px] overflow-hidden rounded-[22px] shadow-[0_0_0_1px_var(--border)]"> <div ref={scroller} className="h-full overflow-y-auto" tabIndex={0} role="region" aria-label="Messages"> <ul className="grid"> {Array.from({ length: 24 }, (_, index) => ( <li key={index} className="flex items-center gap-3 border-b border-border px-4 py-3"> <Avatar name={names[index % names.length]!} className="size-8 rounded-[10px] text-[11px]" /> <span className="grid min-w-0"> <span className="truncate text-sm font-medium">{names[index % names.length]}</span> <span className="truncate text-xs text-muted-foreground">{actions[index % actions.length]}</span> </span> </li> ))} </ul> </div> <ScrollAwareFab icon={<PenLine />} label="New message" scrollRoot={scroller} position="absolute" /> </div> ); }
API reference #
ScrollAwareFab
component
Also accepts the native props of its root element and a ref.
Accessibility #
It is built on native elements and keeps aria-label and aria-hidden in step with state.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.