Skip to main content
Copy for LLM

Scroll to Top

The <x-wirekit::scroll-to-top> component renders a fixed-position button that appears after the user scrolls past a configurable viewport depth. Clicking it smooth-scrolls back to the top of the page.

Basic Usage

Scroll to Top Button
{{-- Place once in your layout, typically at the end of <body> --}}
<x-wirekit::scroll-to-top />

The button is invisible by default and fades in once the user scrolls past 1.5x the viewport height. It positions itself in the bottom-right corner.

Custom Threshold

The threshold prop controls when the button appears, expressed as a multiplier of the viewport height (of the main region's height in an application shell):

{{-- Appear early (after half a screen of scrolling) --}}
<x-wirekit::scroll-to-top :threshold="0.5" />

{{-- Appear late (after 3 full screens) --}}
<x-wirekit::scroll-to-top :threshold="3" />

{{-- Default: 1.5x viewport height --}}
<x-wirekit::scroll-to-top />
Value Meaning
0.5 After scrolling half the viewport
1 After scrolling one full viewport
1.5 After scrolling 1.5 viewports (default)
3 After scrolling 3 viewports

Size Variants

<x-wirekit::scroll-to-top size="sm" />  {{-- Compact --}}
<x-wirekit::scroll-to-top size="md" />  {{-- Default --}}
<x-wirekit::scroll-to-top size="lg" />  {{-- Large, touch-friendly --}}

Position

The button defaults to the bottom-right corner. Use the position prop to move it:

<x-wirekit::scroll-to-top position="bottom-right" />  {{-- Default --}}
<x-wirekit::scroll-to-top position="bottom-left" />
<x-wirekit::scroll-to-top position="top-right" />
<x-wirekit::scroll-to-top position="top-left" />

The offset from the edge uses the --padding-wk-x-lg design token, so it adapts to your theme.

Over a floating action button

A floating action button in the same bottom corner takes the spot the button would sit in. With above-fab the button stands over it instead, centered on it and one --gap-wk-sm above it:

{{-- 1. The floating action button in the bottom-right corner. --}}
<x-wirekit::fab.button label="New invoice" icon="plus" />

{{-- 2. The button back to the top stands over it, centered on it. --}}
<x-wirekit::scroll-to-top above-fab />

Both read the floating action button's own tokens: its size, which steps down on a phone, its distance from the edge, and the --wk-fab-lift you set for a banner along the bottom of the page. The pair moves together. For a floating action button at the start, give both the same side: position="start" on the floating action button and position="bottom-left" here. In a top corner above-fab changes nothing, since there is no button below to stand on.

In an application shell

In an application shell with viewport the window does not scroll: the shell is as tall as the window, and the main region scrolls inside it. The button follows the region that scrolls the page, so the same tag works there and in a page that scrolls as a whole. It shows once that region has scrolled threshold times its own height, and a click brings that region back to the top.

{{-- 1. The shell is as tall as the window, and the main region scrolls. --}}
{{-- 2. The button inside it follows the main region, not the window. --}}
<x-wirekit::app-shell viewport>
    <x-wirekit::main>
        {{ $slot }}

        <x-wirekit::scroll-to-top />
    </x-wirekit::main>
</x-wirekit::app-shell>

Placed beside the shell instead, at the end of <body> in your layout, the button finds the main region all the same. It looks first for the scroll container it stands in, then for the shell's main region, and otherwise watches the window.

How It Works

  1. A scroll listener on the document, in the capture phase and passive, hears the page and any region that scrolls inside it
  2. requestAnimationFrame throttles updates to avoid layout thrashing
  3. When the region that scrolls the page has passed threshold times its own height (window.scrollY > window.innerHeight * threshold on a page that scrolls as a whole), the button fades in via x-transition
  4. Clicking calls scrollTo({ top: 0, behavior: 'smooth' }) on that region, or on the window, for native smooth scrolling
  5. The destroy() lifecycle hook removes the scroll listener on component teardown (e.g. Livewire navigation)

Hiding it yourself

An x-show or wire:show you write on the tag hides the button in addition to its own threshold. The button already shows and hides itself, so your directive goes on a <div class="contents"> around it, which generates no box, and the button keeps waiting for the threshold inside. A child selector in your own CSS that reached the button directly no longer matches once you add one.

{{-- 1. Your state decides whether the button may appear at all. --}}
{{-- 2. The threshold still decides when it appears. --}}
<div x-data="{ cartOpen: false }">
    <x-wirekit::scroll-to-top x-show="! cartOpen" />
</div>

Bringing a result into view

This button takes a reader back to the top. For the other direction, a result that an action makes appear where the reader is not looking, there is x-wk-scroll-into-view: the region comes into view and takes the focus.

Props

Prop Type Default Description
threshold float 1.5 Height multiplier — the button appears once the page, or the region that scrolls it, has scrolled threshold times its own height
size string 'md' 'sm', 'md', 'lg'
position string 'bottom-right' 'bottom-right', 'bottom-left', 'top-right', 'top-left'
aboveFab bool false Stand over a floating action button in the same bottom corner, centered on it, one --gap-wk-sm above it. See Over a floating action button
forceVisible bool false Show the button regardless of scroll position. For a preview or a screenshot, where there is nothing to scroll
scope string|null null Scoped personalization key

Accessibility

  • <button type="button"> — semantically correct, keyboard-focusable
  • aria-label="Scroll to top" — announces the action to screen readers
  • Visible focus ring via focus-visible:ring-* design tokens
  • SVG icon is aria-hidden="true" — purely decorative
  • Under reduced motion, from the system or from data-reduce-motion="reduce" on <html>, it jumps to the top instead of gliding, also on a page that sets scroll-behavior: smooth on its root
  • Activating it from the keyboard moves the focus to the first control of the page as well, so the next Tab continues from the top rather than from where the button was
  • It marks the band its corner covers, so a control a reader tabs to comes to rest clear of the button rather than under it (keeping the focus clear of a pinned bar). At the very end of a page, where nothing is left to scroll, leave room under the last control: a bottom padding of about 4rem on the page keeps it clear
  • x-cloak prevents flash of the button before Alpine initializes

Keyboard Interaction

Key Action
Tab Move focus to the button
Enter / Space Activate the button

Pitfalls

  • Don't show scroll-to-top above the fold. The component auto-shows after 150vh of scroll by default (threshold is 1.5) — overriding the threshold to "always visible" defeats the disclosure pattern.

Design Tokens

Token Purpose
--color-wk-accent Button background
--color-wk-accent-fg Arrow icon color
--color-wk-accent-hover Hover background
--shadow-wk-lg Drop shadow for elevation
--z-wk-sticky Z-index (above content, below overlays)
--size-wk-sm / md / lg Button dimensions per size
--padding-wk-x-lg Offset from viewport edge
--ring-wk-width Focus ring width
--transition-wk-duration Hover/show animation speed

Customization

Override defaults in config/wirekit.php:

'components' => [
    'scroll-to-top' => ['size' => 'lg'],
],

Personalization

Override classes globally:

use Pushery\WireKit\WireKit;

WireKit::personalize('scroll-to-top', [
    // A closure, not a string. The shipped block carries the focus ring AND `wk-touch-target`,
    // which expands the tap area to 44x44 on a coarse pointer without moving the painted box —
    // a string override drops both, and neither absence is visible until somebody tabs to the
    // button or reaches for it on a phone.
    'base' => fn (string $vendor): string => $vendor
        .' bg-[var(--color-wk-bg-inverse)] text-[color:var(--color-wk-bg)]',
]);

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store