Back Top
back-top ui Scroll-to-top floating button. Appears after the target container scrolls past a threshold and smooth-scrolls back on click. Supports custom target container, visibility threshold, scroll behavior, custom icon slot, four edge anchors, size variants, and edge offset.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/back-top.json $ npx shadcn@latest add https://uipkge.dev/r/react/back-top.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/back-top.json $ bunx shadcn@latest add https://uipkge.dev/r/react/back-top.json Named registry:
npx shadcn@latest add @uipkge-react/back-top Installs to: components/ui/back-top/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
size | 'sm''default''lg' | default | optional |
position | 'bottom-right''bottom-left''top-right''top-left' | bottom-right | optional |
threshold Visibility threshold in pixels. Button appears once scroll passes it. | number | — | optional |
target Target container. Defaults to the window. Pass a CSS selector or an HTMLElement. | string | HTMLElement | Window | — | optional |
behavior Scroll behavior: 'smooth' or 'auto' (instant). | ScrollBehavior | — | optional |
offset Distance from the viewport edge (px). | number | — | optional |
absolute Use absolute positioning (for section-level containers) instead of fixed (viewport). | boolean | — | optional |
ariaLabel Accessible label. | string | — | optional |
icon Override the default arrow icon. | React.ReactNode | — | optional |
onVisible Fired whenever visibility toggles. | (visible: boolean) => void | — | optional |
npm dependencies
Files installed (3)
-
components/ui/back-top/BackTop.tsx 5.6 kB
import * as React from 'react' import { ArrowUp } from 'lucide-react' import { cn } from '@/lib/utils' import { backTopVariants, type BackTopVariants } from './back-top.variants' export interface BackTopProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'content'> { /** Visibility threshold in pixels. Button appears once scroll passes it. */ threshold?: number /** Target container. Defaults to the window. Pass a CSS selector or an HTMLElement. */ target?: string | HTMLElement | Window /** Scroll behavior: 'smooth' or 'auto' (instant). */ behavior?: ScrollBehavior /** Size variant. */ size?: BackTopVariants['size'] /** Edge anchor position. */ position?: BackTopVariants['position'] /** Distance from the viewport edge (px). */ offset?: number /** Use absolute positioning (for section-level containers) instead of fixed (viewport). */ absolute?: boolean /** Accessible label. */ ariaLabel?: string /** Override the default arrow icon. */ icon?: React.ReactNode /** Fired whenever visibility toggles. */ onVisible?: (visible: boolean) => void } type ScrollTarget = HTMLElement | Window | null const BackTop = React.forwardRef<HTMLButtonElement, BackTopProps>( ( { className, threshold = 200, target, behavior = 'smooth', size = 'default', position = 'bottom-right', offset = 24, absolute = false, ariaLabel = 'Scroll to top', icon, onVisible, onClick, style, ...props }, ref, ) => { const [visible, setVisible] = React.useState(false) // Keep the button mounted during the exit animation, then unmount. const [mounted, setMounted] = React.useState(false) const [dataState, setDataState] = React.useState<'open' | 'closed'>('closed') const currentTargetRef = React.useRef<ScrollTarget>(null) const onVisibleRef = React.useRef(onVisible) onVisibleRef.current = onVisible function resolveTarget(): ScrollTarget { if (typeof window === 'undefined') return null if (target === undefined || target === null) return window if (typeof target === 'string') { const el = document.querySelector<HTMLElement>(target) return el ?? window } return target } function getScrollTop(el: HTMLElement | Window): number { if (el === window) { return window.scrollY ?? document.documentElement.scrollTop ?? document.body.scrollTop ?? 0 } return (el as HTMLElement).scrollTop } function scrollToTop(el: HTMLElement | Window) { if (el === window) { window.scrollTo({ top: 0, behavior }) } else { ;(el as HTMLElement).scrollTo({ top: 0, behavior }) } } const handleScroll = React.useCallback(() => { const current = currentTargetRef.current if (!current) return const scrollTop = getScrollTop(current) const next = scrollTop > threshold setVisible((prev) => { if (next !== prev) { onVisibleRef.current?.(next) return next } return prev }) }, [threshold]) const handleClick = React.useCallback( (e: React.MouseEvent<HTMLButtonElement>) => { onClick?.(e) const current = currentTargetRef.current if (current) scrollToTop(current) }, [onClick, behavior], ) // Mount/unmount with exit animation, mirroring the Vue <Transition name="back-top">. React.useEffect(() => { if (visible) { setMounted(true) setDataState('open') return } if (mounted) { setDataState('closed') const t = window.setTimeout(() => setMounted(false), 200) return () => window.clearTimeout(t) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]) // Attach scroll listeners; re-resolve when the target prop changes. React.useEffect(() => { currentTargetRef.current = resolveTarget() const current = currentTargetRef.current if (!current) return current.addEventListener('scroll', handleScroll, { passive: true }) // If target is a container, also listen to window scroll for safety on resize. if (current !== window) { window.addEventListener('scroll', handleScroll, { passive: true }) } handleScroll() return () => { current.removeEventListener('scroll', handleScroll) if (current !== window) { window.removeEventListener('scroll', handleScroll) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [target, handleScroll]) const positionStyle = React.useMemo<React.CSSProperties>(() => { const o = `${offset}px` switch (position) { case 'bottom-left': return { left: o, bottom: o } case 'top-right': return { right: o, top: o } case 'top-left': return { left: o, top: o } default: return { right: o, bottom: o } } }, [position, offset]) if (!mounted) return null return ( <button ref={ref} data-uipkge="" data-slot="back-top" data-state={dataState} data-size={size ?? undefined} data-position={position ?? undefined} type="button" aria-label={ariaLabel} className={cn(backTopVariants({ size, position }), absolute ? 'absolute' : 'fixed', className)} style={{ ...positionStyle, ...style }} onClick={handleClick} {...props} > {icon ?? <ArrowUp aria-hidden="true" />} </button> ) }, ) BackTop.displayName = 'BackTop' export { BackTop } -
components/ui/back-top/back-top.variants.ts 1.5 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so `BackTop.tsx` can `import { backTopVariants } from * './back-top.variants'` without creating a circular dependency through the * index. See card.variants.ts for the same pattern. */ export const backTopVariants = cva( 'inline-flex items-center justify-center rounded-full border bg-background text-foreground shadow-lg transition-all duration-200 hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none cursor-pointer motion-safe:data-[state=open]:animate-in motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=closed]:zoom-out-95 [&_svg:not([class*=size-])]:size-5 [&_svg]:pointer-events-none [&_svg]:shrink-0', { variants: { size: { sm: 'size-8 [&_svg:not([class*=size-])]:size-4', default: 'size-10', lg: 'size-12 [&_svg:not([class*=size-])]:size-6', }, position: { 'bottom-right': '', 'bottom-left': '', 'top-right': '', 'top-left': '', }, }, defaultVariants: { size: 'default', position: 'bottom-right', }, }, ) export type BackTopVariants = VariantProps<typeof backTopVariants> -
components/ui/back-top/index.ts 0.1 kB
export { BackTop, type BackTopProps } from './BackTop' export { backTopVariants, type BackTopVariants } from './back-top.variants'
Raw manifest: https://uipkge.dev/r/react/back-top.json