feat: add View Transitions API hero animation for favicon between pages

The favicon animates from its large position in the WelcomeWorld title
to the smaller header position in PublicNoteListView and PublicNoteListByDidView.
This commit is contained in:
Julien Calixte
2026-03-19 18:43:26 +01:00
parent ddabe5082d
commit 29e56304c4
5 changed files with 43 additions and 1 deletions

View File

@@ -21,4 +21,14 @@ const { isATProtoReady } = useATProtoLogin()
display: flex; display: flex;
flex: 1; flex: 1;
} }
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.25s;
}
::view-transition-group(remanso-logo) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}
</style> </style>

View File

@@ -14,7 +14,7 @@ const { userInput, repoInput, submit } = useForm()
<template> <template>
<div class="welcome-world"> <div class="welcome-world">
<h1 class="title is-1"> <h1 class="title is-1">
<img src="/favicon.png" alt="Remanso icon" /> <img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
Remanso Remanso
</h1> </h1>
@@ -93,6 +93,10 @@ h1 {
} }
} }
.remanso-logo {
view-transition-name: remanso-logo;
}
.welcome-world { .welcome-world {
padding: 1rem; padding: 1rem;
margin: auto; margin: auto;

View File

@@ -1,3 +1,4 @@
import { nextTick } from "vue"
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router" import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"
import Home from "@/views/HomeApp.vue" import Home from "@/views/HomeApp.vue"
@@ -93,3 +94,13 @@ export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes, routes,
}) })
router.beforeEach(() => {
if (!("startViewTransition" in document)) return
return new Promise<void>((resolve) => {
;(document as Document & { startViewTransition: (cb: () => Promise<void>) => void }).startViewTransition(async () => {
resolve()
await nextTick()
})
})
})

View File

@@ -20,6 +20,7 @@ const author = computedAsync(async () => getAuthor(did.value))
<main class="public-note-list-view"> <main class="public-note-list-view">
<div class="header"> <div class="header">
<home-button class="back-button" /> <home-button class="back-button" />
<img src="/favicon.png" alt="Remanso" class="remanso-logo" />
<h1 v-if="author">{{ author.handle }}</h1> <h1 v-if="author">{{ author.handle }}</h1>
<div v-else class="skeleton h-8 w-40"></div> <div v-else class="skeleton h-8 w-40"></div>
</div> </div>
@@ -65,6 +66,13 @@ const author = computedAsync(async () => getAuthor(did.value))
position: absolute; position: absolute;
} }
.remanso-logo {
width: 32px;
height: 32px;
box-shadow: none;
view-transition-name: remanso-logo;
}
@media screen and (min-width: 769px) { @media screen and (min-width: 769px) {
overflow-y: auto; overflow-y: auto;
} }

View File

@@ -34,6 +34,7 @@ const following = useFollowingNoteList(follows, followingEnabled)
<main class="public-note-list-view"> <main class="public-note-list-view">
<div class="header"> <div class="header">
<home-button class="back-button" /> <home-button class="back-button" />
<img src="/favicon.png" alt="Remanso" class="remanso-logo" />
</div> </div>
<div v-if="isLoggedIn" role="tablist" class="tabs tabs-border"> <div v-if="isLoggedIn" role="tablist" class="tabs tabs-border">
@@ -135,6 +136,14 @@ const following = useFollowingNoteList(follows, followingEnabled)
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
position: absolute;
}
.remanso-logo {
width: 32px;
height: 32px;
box-shadow: none;
view-transition-name: remanso-logo;
} }
@media screen and (min-width: 769px) { @media screen and (min-width: 769px) {