Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial View Transition Support #7511

Merged
merged 57 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
fbb925d
Basic support
matthewp Jun 28, 2023
429be8e
Add the fade transition
matthewp Jun 28, 2023
aa32aaa
Move CSS into a separate file
matthewp Jun 28, 2023
cec93b8
Add transition name
matthewp Jun 28, 2023
6f0cf0b
View Transitions changeset
matthewp Jun 28, 2023
848db48
Replace the boolean transition with 'morph'
matthewp Jun 28, 2023
7d6966c
Update to use `transition:animate`
matthewp Jun 29, 2023
684dce6
Use head propagation
matthewp Jun 30, 2023
8b9764f
Move CSS into a separate file
matthewp Jun 28, 2023
862efda
Add builtin animations and namespaced module
matthewp Jun 30, 2023
3e1d465
Misquote
matthewp Jul 6, 2023
203298e
Remove unused code
matthewp Jul 5, 2023
375726d
Add automatic prefetching to the View Transitions router
matthewp Jul 6, 2023
dfda9c3
Use a data attribute for back nav animations
matthewp Jul 6, 2023
5253ff0
Use [data-astro-transition]
matthewp Jul 6, 2023
8f69981
Add view transitions to examples
matthewp Jul 6, 2023
692083f
Wait on the HTML response before calling startViewTransition
matthewp Jul 7, 2023
6e9be38
Updated stuff
matthewp Jul 11, 2023
c9690ec
Update the compiler
matthewp Jul 11, 2023
370f54a
Fix
matthewp Jul 11, 2023
dbf9398
Fallback support
matthewp Jul 12, 2023
4f6401b
Properly do fallback
matthewp Jul 12, 2023
90ca262
Simplify the selectors
matthewp Jul 13, 2023
1649231
Put viewTransitions support behind a flag
matthewp Jul 13, 2023
454f414
Upgrade the compiler
matthewp Jul 13, 2023
4f78b3c
Remove unused import
matthewp Jul 13, 2023
190695d
Add tests
matthewp Jul 13, 2023
965a56e
Use an explicit import instead of types
matthewp Jul 13, 2023
3df49c0
Fix case where the click comes from within nested content
matthewp Jul 13, 2023
8d4d2a8
Fix linting
matthewp Jul 13, 2023
11cc735
Merge branch 'main' into view-transitions
matthewp Jul 13, 2023
8658ec6
Add a test for the back button
matthewp Jul 13, 2023
7e38b0c
Merge branch 'main' into view-transitions
matthewp Jul 14, 2023
6645b6a
Prevent glitch in fallback
matthewp Jul 17, 2023
cf63e4e
Do not combine selectors
matthewp Jul 17, 2023
034f4c1
Fallback to MPA nav if there is an issue fetching
matthewp Jul 17, 2023
5c606fd
Fallback swap if there are no animations
matthewp Jul 17, 2023
b3c79d4
Update packages/astro/src/@types/astro.ts
matthewp Jul 18, 2023
35abfbb
Update packages/astro/components/ViewTransitions.astro
matthewp Jul 18, 2023
ba8d7f3
Update packages/astro/components/ViewTransitions.astro
matthewp Jul 18, 2023
2b15915
Update the changeset
matthewp Jul 18, 2023
2c9d9dd
PR review changes
matthewp Jul 18, 2023
0067db0
Update more based on review comments.
matthewp Jul 18, 2023
2492264
Update the updateDOM default
matthewp Jul 18, 2023
26525a1
Merge branch 'main' into view-transitions
matthewp Jul 18, 2023
a34e643
Pass in transitions options to the compiler
matthewp Jul 19, 2023
5361a84
Update broken tests
matthewp Jul 19, 2023
ddbeb56
Update .changeset/silly-garlics-live.md
matthewp Jul 19, 2023
3757f6b
Update .changeset/silly-garlics-live.md
matthewp Jul 19, 2023
bbf7d94
Update .changeset/silly-garlics-live.md
matthewp Jul 19, 2023
78e83b1
Update .changeset/silly-garlics-live.md
matthewp Jul 19, 2023
12d4f70
h2 -> h4
matthewp Jul 19, 2023
d4ad84f
Upgrade to stable compiler
matthewp Jul 19, 2023
b17c5d3
Merge branch 'main' into view-transitions
matthewp Jul 19, 2023
fee0145
Remove exp redirects from sitemap
matthewp Jul 19, 2023
d6e96be
Remove usage from examples
matthewp Jul 19, 2023
ecfadd7
Remove example updates
matthewp Jul 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions .changeset/silly-garlics-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
'astro': minor
---

Built-in View Transitions Support (experimental)

Astro now supports [view transitions](https://developer.chrome.com/docs/web-platform/view-transitions/) through the new `<ViewTransitions />` component and the `transition:animate` (and associated) directives. View transitions are a great fit for content-oriented sites, and we see it as the best path to get the benefits of client-side routing (smoother transitions) without sacrificing the more simple mental model of MPAs.

Enable support for view transitions in Astro 2.9 by adding the experimental flag to your config:

```js
import { defineConfig } from 'astro/config';

export default defineConfig({
experimental: {
viewTransitions: true,
},
})
```

This enables you to use the new APIs added.

#### <ViewTransitions />

This is a component which acts as the *router* for transitions between pages. Add it to the `<head>` section of each individual page where transitions should occur *in the client* as you navigate away to another page, instead of causing a full page browser refresh. To enable support throughout your entire app, add the component in some common layout or component that targets the `<head>` of every page.

__CommonHead.astro__

```astro
---
import { ViewTransitions } from 'astro:transitions';
---

<meta charset="utf-8">
<title>{Astro.props.title}</title>
<ViewTransitions />
```

With only this change, your app will now route completely in-client. You can then add transitions to individual elements using the `transition:animate` directive.

#### Animations

Add `transition:animate` to any element to use Astro's built-in animations.

```astro
<header transition:animate="slide">
```

In the above, Astro's `slide` animation will cause the `<header>` element to slide out to the left, and then slide in from the right when you navigate away from the page.

You can also customize these animations using any CSS animation properties, for example, by specifying a duration:

```astro
---
import { slide } from 'astro:transition';
---
<header transition:animate={slide({ duration: 200 })}>
```

#### Continue learning

Check out the [client-side routing docs](https://docs.astro.build/en/guides/client-side-routing/) to learn more.
2 changes: 1 addition & 1 deletion examples/basics/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
---
---
interface Props {
title: string;
}
Expand Down
1 change: 0 additions & 1 deletion examples/portfolio/src/components/MainHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const {
href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,400;0,700;1,400&family=Rubik:wght@500;600&display=swap"
rel="stylesheet"
/>

<script is:inline>
// This code is inlined in the head to make dark mode instant & blocking.
const getThemePreference = () => {
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/client-base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ declare module 'astro:assets' {
export const { getImage, getConfiguredImageService, Image }: AstroAssets;
}

declare module 'astro:transitions' {
type TransitionModule = typeof import('./dist/transitions/index.js');
export const slide: TransitionModule['slide'];
export const fade: TransitionModule['fade'];

type ViewTransitionsModule = typeof import('./components/ViewTransitions.astro');
export const ViewTransitions: ViewTransitionsModule['default'];
}

type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>;
interface ExportedMarkdownModuleEntities {
frontmatter: MD['frontmatter'];
Expand Down
146 changes: 146 additions & 0 deletions packages/astro/components/ViewTransitions.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
type Fallback = 'none' | 'animate' | 'swap';

export interface Props {
fallback?: Fallback;
}

const { fallback = 'animate' } = Astro.props as Props;
---
<meta name="astro-view-transitions-enabled" content="true">
<meta name="astro-view-transitions-fallback" content={fallback}>
<script>
type Fallback = 'none' | 'animate' | 'swap';
type Direction = 'forward' | 'back';

// The History API does not tell you if navigation is forward or back, so
// you can figure it using an index. On pushState the index is incremented so you
// can use that to determine popstate if going forward or back.
let currentHistoryIndex = history.state?.index || 0;
if(!history.state) {
history.replaceState({index: currentHistoryIndex}, document.title);
}

const supportsViewTransitions = !!document.startViewTransition;
const transitionEnabledOnThisPage = () => !!document.querySelector('[name="astro-view-transitions-enabled"]');

async function getHTML(href: string) {
const res = await fetch(href)
const html = await res.text();
return { ok: res.ok, html };
}

function getFallback(): Fallback {
const el = document.querySelector('[name="astro-view-transitions-fallback"]');
if(el) {
return el.getAttribute('content') as Fallback;
}
return 'animate';
}

const parser = new DOMParser();

async function updateDOM(dir: Direction, html: string, fallback?: Fallback) {
const doc = parser.parseFromString(html, 'text/html');
doc.documentElement.dataset.astroTransition = dir;
const swap = () => document.documentElement.replaceWith(doc.documentElement);

if(fallback === 'animate') {
let isAnimating = false;
addEventListener('animationstart', () => isAnimating = true, { once: true });

// Trigger the animations
document.documentElement.dataset.astroTransitionFallback = 'old';
doc.documentElement.dataset.astroTransitionFallback = 'new';
// If there are any animations, want for the animationend event.
addEventListener('animationend', swap, { once: true });
// If there are no animations, go ahead and swap on next tick
// This is necessary because we do not know if there are animations.
// The setTimeout is a fallback in case there are none.
setTimeout(() => !isAnimating && swap());
matthewp marked this conversation as resolved.
Show resolved Hide resolved
} else {
swap();
}
}

async function navigate(dir: Direction, href: string) {
let finished: Promise<void>;
const { html, ok } = await getHTML(href);
// If there is a problem fetching the new page, just do an MPA navigation to it.
if(!ok) {
location.href = href;
matthewp marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if(supportsViewTransitions) {
finished = document.startViewTransition(() => updateDOM(dir, html)).finished;
} else {
finished = updateDOM(dir, html, getFallback());
}
try {
await finished;
} finally {
document.documentElement.removeAttribute('data-astro-transition');
}
}

// Prefetching
function maybePrefetch(pathname: string) {
if(document.querySelector(`link[rel=prefetch][href="${pathname}"]`)) return;
if(navigator.connection){
let conn = navigator.connection;
if(conn.saveData || /(2|3)g/.test(conn.effectiveType || '')) return;
}
let link = document.createElement('link');
link.setAttribute('rel', 'prefetch');
link.setAttribute('href', pathname);
document.head.append(link);
}

if(supportsViewTransitions || getFallback() !== 'none') {
document.addEventListener('click', (ev) => {
let link = ev.target;
if(link instanceof Element && link.tagName !== 'A') {
link = link.closest('a');
}
// This check verifies that the click is happening on an anchor
// that is going to another page within the same origin. Basically it determines
// same-origin navigation, but omits special key combos for new tabs, etc.
if (link &&
matthewp marked this conversation as resolved.
Show resolved Hide resolved
link instanceof HTMLAnchorElement &&
link.href &&
(!link.target || link.target === '_self') &&
link.origin === location.origin &&
ev.button === 0 && // left clicks only
!ev.metaKey && // new tab (mac)
!ev.ctrlKey && // new tab (windows)
!ev.altKey && // download
!ev.shiftKey &&
!ev.defaultPrevented &&
transitionEnabledOnThisPage()
) {
ev.preventDefault();
navigate('forward', link.href);
currentHistoryIndex++;
history.pushState({index: currentHistoryIndex}, '', link.href);
}
});
window.addEventListener('popstate', () => {
if(!transitionEnabledOnThisPage()) return;
const nextIndex = history.state?.index ?? (currentHistoryIndex + 1);
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
navigate(direction, location.href);
currentHistoryIndex = nextIndex;
});

['mouseenter', 'touchstart', 'focus'].forEach(evName => {
document.addEventListener(evName, ev => {
if(ev.target instanceof HTMLAnchorElement) {
let el = ev.target;
if(el.origin === location.origin && el.pathname !== location.pathname && transitionEnabledOnThisPage()) {
maybePrefetch(el.pathname);
}
}
}, { passive: true, capture: true });
});
}
</script>
1 change: 1 addition & 0 deletions packages/astro/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as Code } from './Code.astro';
export { default as Debug } from './Debug.astro';
export { default as ViewTransitions } from './ViewTransitions.astro';
32 changes: 32 additions & 0 deletions packages/astro/components/viewtransitions.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@keyframes astroFadeInOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

@keyframes astroFadeIn {
from { opacity: 0; }
}

@keyframes astroFadeOut {
to { opacity: 0; }
}

@keyframes astroSlideFromRight {
from { transform: translateX(100%); }
}

@keyframes astroSlideFromLeft {
from { transform: translateX(-100%); }
}

@keyframes astroSlideToRight {
to { transform: translateX(100%); }
}

@keyframes astroSlideToLeft {
to { transform: translateX(-100%); }
}
8 changes: 8 additions & 0 deletions packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
experimental: {
viewTransitions: true,
},
});
8 changes: 8 additions & 0 deletions packages/astro/e2e/fixtures/view-transitions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@e2e/view-transitions",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<title>Testing</title>
<ViewTransitions />
</head>
<body>
<header transition:animate="morph">
<h1>testing</h1>
</header>
<main transition:animate="slide">
<slot />
</main>
</body>
</html>
12 changes: 12 additions & 0 deletions packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import Layout from '../components/Layout.astro';
---
<Layout>
<p id="four">Page 4</p>
<a id="click-one" href="/one">
<div>
Nested
<span>go to 1</span>
</div>
</a>
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
import Layout from '../components/Layout.astro';
---
<Layout>
<p id="one">Page 1</p>
<a id="click-two" href="/two">go to 2</a>
<a id="click-three" href="/three">go to 3</a>
</Layout>
11 changes: 11 additions & 0 deletions packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<html>
<head>
<title>Page 3</title>
</head>
<body>
<main>
<p id="three">Page 3</p>
<a id="click-two" href="/two">go to 2</a>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
import Layout from '../components/Layout.astro';
---
<Layout>
<p id="two">Page 2</p>
</Layout>