Skip to content
ShineUI
Install

ScrollTop

Back up fast.

<sui-scroll-top>MiscStable

About

Floating back-to-top button. It appears at the bottom trailing corner once the page, or a scrollable target, has scrolled past threshold px, and a click scrolls back to the top. Placed as the last child of a scrolling target it sticks to that box's bottom edge instead of the viewport's. Use on long registers and reports.

Usage

<sui-scroll-top threshold="120"></sui-scroll-top> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-scroll-top"); el.addEventListener("suiVisibleChange", (e) => console.log(e.detail)); </script>
import { SuiScrollTop } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiScrollTop threshold="120" onSuiVisibleChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiScrollTop } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiScrollTop threshold="120" @sui-visible-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiScrollTop } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiScrollTop], template: `<sui-scroll-top threshold="120" [(visible)]="visible"></sui-scroll-top>`, }) export class Example { visible = false; }

Basic

Scroll the register past 120px and the button appears at its bottom trailing corner: the element sits inside its target and sticks to that edge; without a target it watches the window and floats over the page. It is a native button named "Back to top", in the DOM only while visible, and it jumps under reduced motion.

<div class="st-basic" tabindex="0" role="group" aria-label="Invoice register" style="position:relative;height:150px;overflow:auto;border-radius:10px;background:var(--canvas);border:1px solid var(--border)"> <div style="padding:14px;font:460 12.5px/1.7 var(--font-sans);color:var(--ink-muted)" id="st-basic-rows"></div> <sui-scroll-top threshold="120"></sui-scroll-top> </div>
// setup(root): properties and events setup(root) { root.querySelector('#st-basic-rows').innerHTML = register(); root.querySelector('sui-scroll-top').target = root.querySelector('.st-basic'); }

Threshold

threshold is how far the container scrolls before the button shows. Keep it above a screenful on a long register so it does not appear over content the reader can still see.

<div style="display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))"> <div class="st-eager" tabindex="0" role="group" aria-label="Invoice register" style="position:relative;height:150px;overflow:auto;border-radius:10px;background:var(--canvas);border:1px solid var(--border)"> <div style="padding:14px;font:460 12.5px/1.7 var(--font-sans);color:var(--ink-muted)" id="st-eager-rows"></div> <sui-scroll-top threshold="40"></sui-scroll-top> </div> <div class="st-lazy" tabindex="0" role="group" aria-label="Invoice register" style="position:relative;height:150px;overflow:auto;border-radius:10px;background:var(--canvas);border:1px solid var(--border)"> <div style="padding:14px;font:460 12.5px/1.7 var(--font-sans);color:var(--ink-muted)" id="st-lazy-rows"></div> <sui-scroll-top threshold="600"></sui-scroll-top> </div> </div>
// setup(root): properties and events setup(root) { root.querySelector('#st-eager-rows').innerHTML = register(); root.querySelector('#st-lazy-rows').innerHTML = register(); root.querySelector('.st-eager sui-scroll-top').target = root.querySelector('.st-eager'); root.querySelector('.st-lazy sui-scroll-top').target = root.querySelector('.st-lazy'); }

Events

suiVisibleChange fires only on an actual flip, with the new visible as the detail, so a scroll does not produce an event storm. It is notification only and not cancelable: nothing opened this control and there is no trigger to hand focus back to.

Scroll the register.
<div style="display:grid;gap:10px"> <div class="st-events" tabindex="0" role="group" aria-label="Invoice register" style="position:relative;height:150px;overflow:auto;border-radius:10px;background:var(--canvas);border:1px solid var(--border)"> <div style="padding:14px;font:460 12.5px/1.7 var(--font-sans);color:var(--ink-muted)" id="st-events-rows"></div> <sui-scroll-top threshold="120"></sui-scroll-top> </div> <span id="st-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Scroll the register.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#st-log'); const el = root.querySelector('.st-events sui-scroll-top'); let flips = 0; root.querySelector('#st-events-rows').innerHTML = register(); el.addEventListener('suiVisibleChange', (e) => { flips += 1; log.textContent = `visible is ${e.detail}. ${flips} flip(s) so far.`; }); el.target = root.querySelector('.st-events'); }

Keyboard

KeyAction
TabLands on the button once the page has scrolled past the threshold; below it the button is not in the DOM at all.
Enter SpaceScrolls the bound scroller to the top, smoothly unless the reader prefers reduced motion. The button leaves the DOM once the scroll passes back under the threshold, which drops focus to the page, and the element emits suiVisibleChange with false as it goes.

API

<sui-scroll-top>

Properties

PropertyAttributeTypeDefaultDescription
targettargetHTMLElement | string | undefinedScroll container to watch instead of the window: an element as a property, or a CSS selector as an attribute. Writing it re-reads the scroll state. Put the element inside that container, after its content, and the button sticks to the container's bottom edge; anywhere else it floats over the viewport as it does for the window. The reference took a React ref.
thresholdthresholdnumber300Scroll distance in px after which the button appears.
visiblevisiblebooleanfalseWhether the button is showing. Mutable and reflected, so a framework can bind it and a test can pin it with <sui-scroll-top visible>. A pinned value is left alone on mount.

Events

EventDetailReactVueDescription
suiVisibleChangebooleanonSuiVisibleChange@sui-visible-changeFires when the element shows or hides itself from the scroll position; detail is the new visible. Only a flip emits, so a scroll does not produce an event storm. Not cancelable: nothing opened this and there is no trigger to hand focus back to. Does not bubble.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the button while it shows. Does nothing while it is hidden.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also