# Back to Top

> Button that appears after scrolling, with a progress ring and smooth scroll to top.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/back-to-top
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/back-to-top.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/back-to-top-demo.tsx"
"use client"

import * as React from "react"

import { BackToTop } from "@/components/back-to-top"

export default function BackToTopDemo() {
  const scroller = React.useRef<HTMLDivElement>(null)

  return (
    <div className="relative w-full max-w-xl overflow-hidden rounded-2xl border bg-card">
      <div ref={scroller} className="h-80 overflow-y-auto p-6">
        <p className="text-sm text-muted-foreground">Scroll down — the button fades in and its ring fills with your progress.</p>
        {Array.from({ length: 8 }, (_, index) => (
          <div key={index} className="mt-5 h-24 rounded-xl bg-gradient-to-br from-muted to-muted/40" />
        ))}
      </div>
      <BackToTop scrollTarget={scroller} className="absolute right-4 bottom-4" />
    </div>
  )
}
```

## Source

```tsx title="components/back-to-top.tsx"
"use client"

import * as React from "react"
import { ArrowUp } from "lucide-react"

import { cn } from "@/lib/utils"

export interface BackToTopProps extends Omit<React.ComponentProps<"button">, "children" | "onClick"> {
  /** Scroll container to track; the window when omitted */
  scrollTarget?: React.RefObject<HTMLElement | null>
  /** Pixels scrolled before the button appears */
  threshold?: number
}

const RADIUS = 20
const CIRCUMFERENCE = 2 * Math.PI * RADIUS

export function BackToTop({ scrollTarget, threshold = 160, className, ...props }: BackToTopProps) {
  const [progress, setProgress] = React.useState(0)
  const [visible, setVisible] = React.useState(false)

  React.useEffect(() => {
    const element = scrollTarget?.current
    const source: HTMLElement | Window = element ?? window
    const update = () => {
      const scrolled = element ? element.scrollTop : window.scrollY
      const total = element ? element.scrollHeight - element.clientHeight : document.documentElement.scrollHeight - window.innerHeight
      setProgress(total > 0 ? Math.min(1, scrolled / total) : 0)
      setVisible(scrolled > threshold)
    }
    const frame = requestAnimationFrame(update)
    source.addEventListener("scroll", update, { passive: true })
    return () => {
      cancelAnimationFrame(frame)
      source.removeEventListener("scroll", update)
    }
  }, [scrollTarget, threshold])

  return (
    <button
      type="button"
      aria-label="Back to top"
      tabIndex={visible ? 0 : -1}
      onClick={() => {
        const element = scrollTarget?.current
        const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches
        const behavior = reduce ? "auto" : "smooth"
        if (element) element.scrollTo({ top: 0, behavior })
        else window.scrollTo({ top: 0, behavior })
      }}
      className={cn(
        "group fixed right-6 bottom-6 z-40 flex size-12 items-center justify-center rounded-full border bg-background text-foreground shadow-lg transition-[opacity,transform] duration-300 hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
        visible ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-4 opacity-0",
        className
      )}
      {...props}
    >
      <svg viewBox="0 0 48 48" aria-hidden className="absolute inset-0 size-full -rotate-90">
        <circle cx="24" cy="24" r={RADIUS} fill="none" strokeWidth="2.5" className="stroke-border" />
        <circle
          cx="24"
          cy="24"
          r={RADIUS}
          fill="none"
          strokeWidth="2.5"
          strokeLinecap="round"
          strokeDasharray={CIRCUMFERENCE}
          strokeDashoffset={CIRCUMFERENCE * (1 - progress)}
          className="stroke-primary"
        />
      </svg>
      <ArrowUp className="size-4 transition-transform duration-200 group-hover:-translate-y-0.5" />
    </button>
  )
}
```
