@@ -6,8 +6,10 @@ const excludedRoutes = new Set(['index', 'code'])
66
77const isActive = computed (() => ! excludedRoutes .has (route .name as string ))
88
9+ const isMounted = ref (false )
910const isVisible = ref (false )
1011const scrollThreshold = 300
12+ const supportsScrollStateQueries = ref (false )
1113
1214function onScroll() {
1315 isVisible .value = window .scrollY > scrollThreshold
@@ -18,8 +20,15 @@ function scrollToTop() {
1820}
1921
2022onMounted (() => {
21- window .addEventListener (' scroll' , onScroll , { passive: true })
22- onScroll ()
23+ // Feature detect CSS scroll-state container queries (Chrome 133+)
24+ supportsScrollStateQueries .value = CSS .supports (' container-type' , ' scroll-state' )
25+
26+ if (! supportsScrollStateQueries .value ) {
27+ window .addEventListener (' scroll' , onScroll , { passive: true })
28+ onScroll ()
29+ }
30+
31+ isMounted .value = true
2332})
2433
2534onUnmounted (() => {
@@ -28,7 +37,20 @@ onUnmounted(() => {
2837 </script >
2938
3039<template >
40+ <!-- When CSS scroll-state is supported, use CSS-only visibility -->
41+ <button
42+ v-if =" isActive && supportsScrollStateQueries"
43+ type =" button"
44+ class =" scroll-to-top-css fixed bottom-4 right-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"
45+ aria-label =" Scroll to top"
46+ @click =" scrollToTop"
47+ >
48+ <span class =" i-carbon-arrow-up w-5 h-5" aria-hidden =" true" />
49+ </button >
50+
51+ <!-- JS fallback for browsers without scroll-state support -->
3152 <Transition
53+ v-else
3254 enter-active-class =" transition-all duration-200"
3355 enter-from-class =" opacity-0 translate-y-2"
3456 enter-to-class =" opacity-100 translate-y-0"
@@ -37,7 +59,7 @@ onUnmounted(() => {
3759 leave-to-class =" opacity-0 translate-y-2"
3860 >
3961 <button
40- v-if =" isActive && isVisible"
62+ v-if =" isActive && isMounted && isVisible"
4163 type =" button"
4264 class =" fixed bottom-4 right-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"
4365 aria-label =" Scroll to top"
@@ -47,3 +69,28 @@ onUnmounted(() => {
4769 </button >
4870 </Transition >
4971</template >
72+
73+ <style scoped>
74+ /*
75+ * CSS scroll-state container queries (Chrome 133+)
76+ * Hide button by default, show when page can be scrolled up (user has scrolled down)
77+ */
78+ @supports (container-type : scroll-state) {
79+ .scroll-to-top-css {
80+ opacity : 0 ;
81+ transform : translateY (0.5rem );
82+ pointer-events : none ;
83+ transition :
84+ opacity 0.2s ease ,
85+ transform 0.2s ease ;
86+ }
87+
88+ @container scroll-state(scrollable: top) {
89+ .scroll-to-top-css {
90+ opacity : 1 ;
91+ transform : translateY (0 );
92+ pointer-events : auto ;
93+ }
94+ }
95+ }
96+ </style >
0 commit comments