11<script setup lang="ts">
2+ const isMounted = ref (false )
23const isVisible = ref (false )
34const isScrollable = ref (true )
45const lastScrollY = ref (0 )
56const footerRef = ref <HTMLElement >()
67
8+ // Check if CSS scroll-state container queries are supported
9+ // Once this becomes baseline, we can remove the JS scroll handling entirely
10+ const supportsScrollStateQueries = ref (false )
11+
712function checkScrollable() {
8- const mainContent = document .getElementById (' main-content' )
9- if (! mainContent ) return true
10- return mainContent .scrollHeight > window .innerHeight
13+ return document .documentElement .scrollHeight > window .innerHeight
1114}
1215
1316function onScroll() {
17+ // Skip JS-based visibility logic if CSS scroll-state queries handle it
18+ if (supportsScrollStateQueries .value ) return
19+
1420 const currentY = window .scrollY
1521 const diff = lastScrollY .value - currentY
1622 const nearBottom = currentY + window .innerHeight >= document .documentElement .scrollHeight - 50
@@ -37,55 +43,72 @@ function updateFooterPadding() {
3743 document .documentElement .style .setProperty (' --footer-height' , ` ${height }px ` )
3844}
3945
46+ function onResize() {
47+ isScrollable .value = checkScrollable ()
48+ updateFooterPadding ()
49+ }
50+
4051onMounted (() => {
52+ // Feature detect CSS scroll-state container queries (Chrome 133+)
53+ // @see https://developer.mozilla.org/en-US/docs/Web/CSS/@container#scroll-state_container_descriptors
54+ supportsScrollStateQueries .value = CSS .supports (' container-type' , ' scroll-state' )
55+
4156 nextTick (() => {
4257 lastScrollY .value = window .scrollY
4358 isScrollable .value = checkScrollable ()
4459 updateFooterPadding ()
60+ // Only apply dynamic classes after mount to avoid hydration mismatch
61+ isMounted .value = true
4562 })
4663
4764 window .addEventListener (' scroll' , onScroll , { passive: true })
48- window .addEventListener (
49- ' resize' ,
50- () => {
51- isScrollable .value = checkScrollable ()
52- updateFooterPadding ()
53- },
54- { passive: true },
55- )
65+ window .addEventListener (' resize' , onResize , { passive: true })
5666})
5767
5868onUnmounted (() => {
5969 window .removeEventListener (' scroll' , onScroll )
70+ window .removeEventListener (' resize' , onResize )
6071})
6172 </script >
6273
6374<template >
6475 <footer
6576 ref =" footerRef"
66- class =" border-t border-border bg-bg"
67- :class ="
68- isScrollable
69- ? [
70- 'fixed bottom-0 left-0 right-0 z-40 transition-transform duration-300 ease-out',
71- isVisible ? 'translate-y-0' : 'translate-y-full',
72- ]
73- : 'mt-auto'
74- "
77+ aria-label =" Site footer"
78+ class =" border-t border-border bg-bg/90 backdrop-blur-md"
79+ :class =" [
80+ // Only apply dynamic positioning classes after mount to avoid hydration mismatch
81+ !isMounted
82+ ? 'mt-auto'
83+ : // When CSS scroll-state queries are supported, use CSS-only approach
84+ supportsScrollStateQueries
85+ ? 'footer-scroll-state'
86+ : // Fallback to JS-controlled classes
87+ isScrollable
88+ ? [
89+ 'fixed bottom-0 left-0 right-0 z-40 transition-transform duration-300 ease-out',
90+ isVisible ? 'translate-y-0' : 'translate-y-full',
91+ ]
92+ : 'mt-auto',
93+ ]"
7594 >
7695 <div class =" container py-6 flex flex-col gap-3 text-fg-subtle text-sm" >
7796 <div class =" flex flex-col sm:flex-row items-center justify-between gap-4" >
7897 <p class =" font-mono m-0" >a better browser for the npm registry</p >
79- <div class =" flex items-center gap-6" >
98+ <div class =" flex items-center gap-4 sm:gap- 6" >
8099 <a
81100 href =" https://github.com/npmx-dev/npmx.dev"
82101 rel =" noopener noreferrer"
83- class =" link-subtle font-mono text-xs"
102+ class =" link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center "
84103 >
85104 source
86105 </a >
87106 <span class =" text-border" >|</span >
88- <a href =" https://roe.dev" rel =" noopener noreferrer" class =" link-subtle font-mono text-xs" >
107+ <a
108+ href =" https://roe.dev"
109+ rel =" noopener noreferrer"
110+ class =" link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center"
111+ >
89112 @danielroe
90113 </a >
91114 </div >
@@ -96,3 +119,33 @@ onUnmounted(() => {
96119 </div >
97120 </footer >
98121</template >
122+
123+ <style scoped>
124+ /*
125+ * CSS scroll-state container queries (Chrome 133+)
126+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/@container#scroll-state_container_descriptors
127+ *
128+ * This provides a pure CSS solution for showing/hiding the footer based on scroll state.
129+ * The JS fallback handles browsers without support.
130+ * Once scroll-state queries become baseline, we can remove the JS scroll handling entirely.
131+ */
132+ @supports (container-type : scroll-state) {
133+ .footer-scroll-state {
134+ position : fixed ;
135+ bottom : 0 ;
136+ left : 0 ;
137+ right : 0 ;
138+ z-index : 40 ;
139+ /* Hidden by default (translated off-screen) */
140+ transform : translateY (100% );
141+ transition : transform 0.3s ease-out ;
142+ }
143+
144+ /* Show footer when user can scroll up (meaning they've scrolled down) */
145+ @container scroll-state(scrollable: top) {
146+ .footer-scroll-state {
147+ transform : translateY (0 );
148+ }
149+ }
150+ }
151+ </style >
0 commit comments