API Reference
Props
| Property |
Descripiton |
Type |
Default |
| icon |
Custom icon |
VNode | Slot |
undefined |
| desrciption |
Text description |
string | Slot |
undefined |
| tooltip |
Tooltip content |
string | Slot |
undefined |
| tooltipProps |
Tooltip configuration |
object |
{} |
| type |
Button style variant |
'default' | 'primary' |
'default' |
| shape |
Button geometry |
'circle' | 'square' |
'circle' |
| bottom |
Distance from bottom (px) |
number | string |
40 |
| right |
Distance from right (px) |
number | string |
40 |
| zIndex |
Stacking context level |
number |
9 |
| visibilityHeight |
Scroll threshold to show button (px) |
number |
180 |
| too |
Mount container selector |
string | HTMLElement |
'body' |
| listenTo |
Scroll event source |
string | HTMLElement |
undefined |
Events
| Event |
Descriptino |
Signature |
| click |
Triggered on button press |
() => void |
| show |
Indicates visibility state change |
(visible: boolean) => void |
Component Implemantation
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import type { VNode } from 'vue'
import TooltipComponent from './Tooltip.vue'
import { useSlotDetection, useDomObserver, useTheme, usePassiveSupport } from './composables'
interface ComponentProps {
icon?: VNode
description?: string
tooltip?: string
tooltipProps?: Record<string, any>
type?: 'default' | 'primary'
shape?: 'circle' | 'square'
bottom?: number | string
right?: number | string
zIndex?: number
visibilityHeight?: number
to?: string | HTMLElement
listenTo?: string | HTMLElement
}
const props = withDefaults(defineProps<ComponentProps>(), {
type: 'default',
shape: 'circle',
bottom: 40,
right: 40,
zIndex: 9,
visibilityHeight: 180,
to: 'body'
})
const emit = defineEmits(['click', 'show'])
const isRendered = ref(false)
const placeholderRef = ref<HTMLElement | null>(null)
const currentScrollY = ref(0)
const scrollSource = ref<HTMLElement | null>(null)
const themeColors = useTheme('BackToTop')
const supportsPassive = usePassiveSupport()
const slotCheck = useSlotDetection(['tooltip', 'icon', 'description'])
const positionStyles = computed(() => ({
bottom: typeof props.bottom === 'number' ? `${props.bottom}px` : props.bottom,
right: typeof props.right === 'number' ? `${props.right}px` : props.right
}))
const shouldDisplay = computed(() => currentScrollY.value >= props.visibilityHeight)
const hasTooltip = computed(() => slotCheck.tooltip || props.tooltip)
const hasDesc = computed(() => slotCheck.description || props.description)
watch(shouldDisplay, (visible) => {
if (visible && !isRendered.value) isRendered.value = true
emit('show', visible)
}, { immediate: true })
let scrollHandler: (e: Event) => void
let domObserver: ReturnType<typeof useDomObserver>
onMounted(initScrollListener)
onBeforeUnmount(removeScrollListener)
function initScrollListener() {
removeScrollListener()
if (props.listenTo) {
scrollSource.value = typeof props.listenTo === 'string'
? document.querySelector(props.listenTo) as HTMLElement
: props.listenTo
} else {
scrollSource.value = findScrollableAncestor(placeholderRef.value)
}
if (!scrollSource.value) return
scrollHandler = (e: Event) => {
currentScrollY.value = (e.target as HTMLElement).scrollTop
}
scrollSource.value.addEventListener(
'scroll',
scrollHandler,
supportsPassive.value ? { passive: true } : false
)
domObserver = useDomObserver(scrollSource, () => {
currentScrollY.value = scrollSource.value?.scrollTop || 0
})
}
function removeScrollListener() {
if (scrollSource.value && scrollHandler) {
scrollSource.value.removeEventListener('scroll', scrollHandler)
}
domObserver?.disconnect()
}
function findScrollableAncestor(el: HTMLElement | null): HTMLElement | null {
if (!el || el === document.documentElement) return document.documentElement
const parent = el.parentElement
if (!parent) return null
const style = window.getComputedStyle(parent)
const isScrollable = /(auto|scroll)/.test(style.overflow + style.overflowY)
return isScrollable ? parent : findScrollableAncestor(parent)
}
function scrollToTop() {
if (scrollSource.value) {
scrollSource.value.scrollTo({ top: 0, behavior: 'smooth' })
emit('click')
}
}
</script>
<template>
<div ref="placeholderRef" style="display: none" />
<Teleport :to="to">
<Transition name="fade-scale">
<div
v-if="isRendered"
v-show="shouldDisplay"
class="back-to-top"
:style="[positionStyles, { zIndex: props.zIndex }]"
@click="scrollToTop"
>
<TooltipComponent v-if="hasTooltip" :content="tooltip" v-bind="tooltipProps">
<div class="button-content" :class="[props.type, props.shape]">
<slot name="icon">
<svg v-if="!icon" viewBox="0 0 24 24" width="1em" height="1em">
<!-- SVG path data -->
</svg>
<component v-else :is="icon" />
</slot>
<span v-if="hasDesc" class="desc">
<slot name="description">{{ description }}</slot>
</span>
</div>
</TooltipComponent>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.fade-scale-enter-active, .fade-scale-leave-active {
transition: all 0.3s ease;
}
.fade-scale-enter-from, .fade-scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
.back-to-top {
position: fixed;
}
.button-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
/* Additional styling */
</style>
Usage Examples
<script setup lang="ts">
import { ref } from 'vue'
import BackToTop from './BackToTop.vue'
import { createIcon } from './icons'
const scrollArea = ref<HTMLElement | null>(null)
const customStyles = {
'--btn-size': '50px',
'--icon-size': '28px',
'--primary-color': '#c2410c'
}
function handleVisibilityChange(visible: boolean) {
console.log('Button visibility:', visible)
}
</script>
<template>
<div>
<BackToTop @show="handleVisibilityChange" />
<BackToTop :icon="createIcon('arrow-up')" :right="120" />
<BackToTop
type="primary"
shape="square"
description="Top"
:bottom="100"
/>
<BackToTop
:tooltip="'Return to top'"
:tooltip-props="{ delay: 300 }"
:right="200"
/>
<BackToTop
:style="customStyles"
:visibility-height="400"
:listen-to="scrollArea"
/>
<div ref="scrollArea" style="height: 200px; overflow: auto">
<!-- Scrollable content -->
</div>
</div>
</template>