forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1<script setup lang="ts">
2const route = useRoute()
3
4// Pages where scroll-to-top should NOT be shown
5const excludedRoutes = new Set(['index', 'code'])
6
7const isActive = computed(() => !excludedRoutes.has(route.name as string))
8
9const isMounted = useMounted()
10const isVisible = shallowRef(false)
11const scrollThreshold = 300
12const { isSupported: supportsScrollStateQueries } = useCssSupports(
13 'container-type',
14 'scroll-state',
15 { ssrValue: false },
16)
17
18function onScroll() {
19 if (!supportsScrollStateQueries.value) {
20 return
21 }
22 isVisible.value = window.scrollY > scrollThreshold
23}
24
25function scrollToTop() {
26 window.scrollTo({ top: 0, behavior: 'smooth' })
27}
28
29useEventListener('scroll', onScroll, { passive: true })
30
31onMounted(() => {
32 onScroll()
33})
34</script>
35
36<template>
37 <!-- When CSS scroll-state is supported, use CSS-only visibility -->
38 <button
39 v-if="isActive && supportsScrollStateQueries"
40 type="button"
41 class="scroll-to-top-css fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg md:hidden flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
42 :aria-label="$t('common.scroll_to_top')"
43 @click="scrollToTop"
44 >
45 <span class="i-lucide:arrow-up w-5 h-5" aria-hidden="true" />
46 </button>
47
48 <!-- JS fallback for browsers without scroll-state support -->
49 <Transition
50 v-else
51 enter-active-class="transition-all duration-200"
52 enter-from-class="opacity-0 translate-y-2"
53 enter-to-class="opacity-100 translate-y-0"
54 leave-active-class="transition-all duration-200"
55 leave-from-class="opacity-100 translate-y-0"
56 leave-to-class="opacity-0 translate-y-2"
57 >
58 <button
59 v-if="isActive && isMounted && isVisible"
60 type="button"
61 class="fixed bottom-4 inset-ie-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg md:hidden flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95"
62 :aria-label="$t('common.scroll_to_top')"
63 @click="scrollToTop"
64 >
65 <span class="i-lucide:arrow-up w-5 h-5" aria-hidden="true" />
66 </button>
67 </Transition>
68</template>
69
70<style scoped>
71/*
72 * CSS scroll-state container queries (Chrome 133+)
73 * Hide button by default, show when page can be scrolled up (user has scrolled down)
74 */
75@supports (container-type: scroll-state) {
76 .scroll-to-top-css {
77 opacity: 0;
78 transform: translateY(0.5rem);
79 pointer-events: none;
80 transition:
81 opacity 0.2s ease,
82 transform 0.2s ease;
83 }
84
85 @container scroll-state(scrollable: top) {
86 .scroll-to-top-css {
87 opacity: 1;
88 transform: translateY(0);
89 pointer-events: auto;
90 }
91 }
92}
93</style>