Skip to content

Commit e36fe19

Browse files
imguoguoclaude
andauthored
feat: add top loading bar for page transitions (#31)
Shows a thin accent-colored progress bar at the top of the page during View Transitions navigation. Uses Astro's built-in lifecycle events: - astro:before-preparation → start progress animation - astro:after-swap → complete to 100% - astro:page-load → clean up Respects prefers-reduced-motion. No external dependencies. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e04397 commit e36fe19

1 file changed

Lines changed: 63 additions & 0 deletions

File tree

src/layouts/Base.astro

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ const {
139139
<ClientRouter />
140140
</head>
141141
<body>
142+
<div id="loading-bar" class="loading-bar" aria-hidden="true"></div>
142143
<slot />
143144
</body>
144145
</html>
@@ -205,4 +206,66 @@ const {
205206
@media (prefers-reduced-motion: reduce) {
206207
.reveal { animation: none; opacity: 1; transform: none; }
207208
}
209+
210+
/* ─── Page loading bar ─── */
211+
.loading-bar {
212+
position: fixed;
213+
top: 0; left: 0;
214+
width: 0;
215+
height: 2.5px;
216+
background: var(--accent);
217+
box-shadow: 0 0 8px var(--accent);
218+
z-index: 9999;
219+
pointer-events: none;
220+
opacity: 0;
221+
transition: opacity .2s;
222+
}
223+
.loading-bar.active {
224+
opacity: 1;
225+
animation: loadingProgress 8s cubic-bezier(.1, .4, .2, .9) forwards;
226+
}
227+
.loading-bar.done {
228+
width: 100% !important;
229+
animation: none;
230+
transition: width .15s ease-out, opacity .3s .1s;
231+
opacity: 0;
232+
}
233+
@keyframes loadingProgress {
234+
0% { width: 0; }
235+
15% { width: 30%; }
236+
50% { width: 60%; }
237+
80% { width: 80%; }
238+
100% { width: 92%; }
239+
}
240+
@media (prefers-reduced-motion: reduce) {
241+
.loading-bar { display: none; }
242+
}
208243
</style>
244+
245+
<script>
246+
// Thin progress bar during View Transitions navigation
247+
document.addEventListener('astro:before-preparation', () => {
248+
const bar = document.getElementById('loading-bar');
249+
if (!bar) return;
250+
bar.classList.remove('done');
251+
bar.classList.add('active');
252+
});
253+
254+
document.addEventListener('astro:after-swap', () => {
255+
// bar element was swapped — re-query from new DOM
256+
const bar = document.getElementById('loading-bar');
257+
if (!bar) return;
258+
bar.classList.remove('active');
259+
bar.classList.add('done');
260+
});
261+
262+
document.addEventListener('astro:page-load', () => {
263+
const bar = document.getElementById('loading-bar');
264+
if (!bar) return;
265+
// Clean up classes after transition is fully done
266+
setTimeout(() => {
267+
bar.classList.remove('active', 'done');
268+
bar.style.width = '';
269+
}, 400);
270+
});
271+
</script>

0 commit comments

Comments
 (0)