Skip to content

Commit b4acb55

Browse files
committed
feat: add CSS scroll-state queries to ScrollToTop component
1 parent b2f021e commit b4acb55

1 file changed

Lines changed: 50 additions & 3 deletions

File tree

app/components/ScrollToTop.vue

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ const excludedRoutes = new Set(['index', 'code'])
66
77
const isActive = computed(() => !excludedRoutes.has(route.name as string))
88
9+
const isMounted = ref(false)
910
const isVisible = ref(false)
1011
const scrollThreshold = 300
12+
const supportsScrollStateQueries = ref(false)
1113
1214
function onScroll() {
1315
isVisible.value = window.scrollY > scrollThreshold
@@ -18,8 +20,15 @@ function scrollToTop() {
1820
}
1921
2022
onMounted(() => {
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
2534
onUnmounted(() => {
@@ -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

Comments
 (0)