Scroll to Top
The <x-wirekit:: 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
{{-- 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
- A
scrolllistener on the document, in the capture phase and passive, hears the page and any region that scrolls inside it requestthrottles updates to avoid layout thrashingAnimation Frame - When the region that scrolls the page has passed
thresholdtimes its own height (window.scrollY > window.on a page that scrolls as a whole), the button fades in viainnerHeight * threshold x-transition - Clicking calls
scrollTo({ top: 0, behavior: 'smooth' })on that region, or on the window, for native smooth scrolling - 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-focusablearia-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 setsscroll-behavior: smoothon 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
4remon the page keeps it clear x-cloakprevents 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 (
thresholdis1.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/:
'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
- MDN:
window.— the smooth scroll APIscrollTo() - MDN:
scrollevent — passive listener for performance - MDN:
request— throttling pattern usedAnimation Frame - MDN:
prefers-reduced-motion— the system preference the button reads