diff --git a/.changeset/silly-garlics-live.md b/.changeset/silly-garlics-live.md
new file mode 100644
index 000000000000..d019b892ce07
--- /dev/null
+++ b/.changeset/silly-garlics-live.md
@@ -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 `` 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.
+
+####
+
+This is a component which acts as the *router* for transitions between pages. Add it to the `
` 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 `` of every page.
+
+__CommonHead.astro__
+
+```astro
+---
+import { ViewTransitions } from 'astro:transitions';
+---
+
+
+{Astro.props.title}
+
+```
+
+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
+
+```
+
+In the above, Astro's `slide` animation will cause the `` 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';
+---
+
+```
+
+#### Continue learning
+
+Check out the [client-side routing docs](https://docs.astro.build/en/guides/client-side-routing/) to learn more.
diff --git a/examples/basics/src/layouts/Layout.astro b/examples/basics/src/layouts/Layout.astro
index 39e868100901..9b9de105c400 100644
--- a/examples/basics/src/layouts/Layout.astro
+++ b/examples/basics/src/layouts/Layout.astro
@@ -1,4 +1,4 @@
----
+---
interface Props {
title: string;
}
diff --git a/examples/portfolio/src/components/MainHead.astro b/examples/portfolio/src/components/MainHead.astro
index 93f1e7bdb26e..b4c7263ff5aa 100644
--- a/examples/portfolio/src/components/MainHead.astro
+++ b/examples/portfolio/src/components/MainHead.astro
@@ -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"
/>
-
diff --git a/packages/astro/components/index.ts b/packages/astro/components/index.ts
index 864c7cc3bbdf..6bd537087eee 100644
--- a/packages/astro/components/index.ts
+++ b/packages/astro/components/index.ts
@@ -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';
diff --git a/packages/astro/components/viewtransitions.css b/packages/astro/components/viewtransitions.css
new file mode 100644
index 000000000000..296c97c2b697
--- /dev/null
+++ b/packages/astro/components/viewtransitions.css
@@ -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%); }
+}
diff --git a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
new file mode 100644
index 000000000000..50ba5b32c61d
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
@@ -0,0 +1,8 @@
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ experimental: {
+ viewTransitions: true,
+ },
+});
diff --git a/packages/astro/e2e/fixtures/view-transitions/package.json b/packages/astro/e2e/fixtures/view-transitions/package.json
new file mode 100644
index 000000000000..50258fd1a16b
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@e2e/view-transitions",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro b/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro
new file mode 100644
index 000000000000..38bc302e92e2
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro
@@ -0,0 +1,17 @@
+---
+import { ViewTransitions } from 'astro:transitions';
+---
+
+
+ Testing
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro
new file mode 100644
index 000000000000..e322937935c0
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/four.astro
@@ -0,0 +1,12 @@
+---
+import Layout from '../components/Layout.astro';
+---
+
+ Page 4
+
+
+ Nested
+ go to 1
+
+
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
new file mode 100644
index 000000000000..e896a857281a
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/one.astro
@@ -0,0 +1,8 @@
+---
+import Layout from '../components/Layout.astro';
+---
+
+ Page 1
+ go to 2
+ go to 3
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
new file mode 100644
index 000000000000..676e8b61be96
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/three.astro
@@ -0,0 +1,11 @@
+
+
+ Page 3
+
+
+
+ Page 3
+ go to 2
+
+
+
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/two.astro
new file mode 100644
index 000000000000..b3cadcad7b71
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/two.astro
@@ -0,0 +1,6 @@
+---
+import Layout from '../components/Layout.astro';
+---
+
+ Page 2
+
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
new file mode 100644
index 000000000000..4744241820c7
--- /dev/null
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -0,0 +1,101 @@
+import { expect } from '@playwright/test';
+import { testFactory } from './test-utils.js';
+
+const test = testFactory({ root: './fixtures/view-transitions/' });
+
+let devServer;
+
+test.beforeAll(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterAll(async () => {
+ await devServer.stop();
+});
+
+test.describe('View Transitions', () => {
+ test('Moving from page 1 to page 2', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', p => {
+ loads.push(p.title());
+ });
+
+ // Go to page 1
+ await page.goto(astro.resolveUrl('/one'));
+ let p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+
+ // go to page 2
+ await page.click('#click-two');
+ p = page.locator('#two');
+ await expect(p, 'should have content').toHaveText('Page 2');
+
+ expect(loads.length, 'There should only be 1 page load').toEqual(1);
+ });
+
+ test('Back button is captured', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', p => {
+ loads.push(p.title());
+ });
+
+ // Go to page 1
+ await page.goto(astro.resolveUrl('/one'));
+ let p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+
+ // go to page 2
+ await page.click('#click-two');
+ p = page.locator('#two');
+ await expect(p, 'should have content').toHaveText('Page 2');
+
+ // Back to page 1
+ await page.goBack();
+ p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+
+ expect(loads.length, 'There should only be 1 page load').toEqual(1);
+ });
+
+ test('Clicking on a link with nested content', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', p => {
+ loads.push(p.title());
+ });
+
+ // Go to page 4
+ await page.goto(astro.resolveUrl('/four'));
+ let p = page.locator('#four');
+ await expect(p, 'should have content').toHaveText('Page 4');
+
+ // Go to page 1
+ await page.click('#click-one');
+ p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+
+ expect(loads.length, 'There should only be 1 page load').toEqual(1);
+ });
+
+ test('Moving from a page without ViewTransitions triggers a full page navigation', async ({ page, astro }) => {
+ const loads = [];
+ page.addListener('load', p => {
+ loads.push(p.title());
+ });
+
+ // Go to page 1
+ await page.goto(astro.resolveUrl('/one'));
+ let p = page.locator('#one');
+ await expect(p, 'should have content').toHaveText('Page 1');
+
+ // Go to page 3 which does *not* have ViewTransitions enabled
+ await page.click('#click-three');
+ p = page.locator('#three');
+ await expect(p, 'should have content').toHaveText('Page 3');
+
+ await page.click('#click-two');
+ p = page.locator('#two');
+ await expect(p, 'should have content').toHaveText('Page 2');
+
+ expect(loads.length, 'There should be 2 page loads. The original, then going from 3 to 2').toEqual(2);
+ });
+});
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 85b4bb19cb18..14bd76757efb 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -71,7 +71,8 @@
"./middleware": {
"types": "./dist/core/middleware/index.d.ts",
"default": "./dist/core/middleware/index.js"
- }
+ },
+ "./transitions": "./dist/transitions/index.js"
},
"imports": {
"#astro/*": "./dist/*.js"
@@ -114,7 +115,7 @@
"test:e2e:match": "playwright test -g"
},
"dependencies": {
- "@astrojs/compiler": "^1.5.3",
+ "@astrojs/compiler": "^1.6.0",
"@astrojs/internal-helpers": "^0.1.1",
"@astrojs/language-server": "^1.0.0",
"@astrojs/markdown-remark": "^2.2.1",
@@ -127,6 +128,7 @@
"@babel/traverse": "^7.22.5",
"@babel/types": "^7.22.5",
"@types/babel__core": "^7.20.1",
+ "@types/dom-view-transitions": "^1.0.1",
"@types/yargs-parser": "^21.0.0",
"acorn": "^8.9.0",
"boxen": "^6.2.1",
@@ -150,6 +152,7 @@
"kleur": "^4.1.4",
"magic-string": "^0.27.0",
"mime": "^3.0.0",
+ "network-information-types": "^0.1.1",
"ora": "^6.3.1",
"p-limit": "^4.0.0",
"path-to-regexp": "^6.2.1",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 0a972d4b07f3..3c171822cc97 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -55,6 +55,27 @@ export interface AstroBuiltinProps {
'client:only'?: boolean | string;
}
+export interface TransitionAnimation {
+ name: string; // The name of the keyframe
+ delay?: number | string;
+ duration?: number | string;
+ easing?: string;
+ fillMode?: string;
+ direction?: string;
+}
+
+export interface TransitionAnimationPair {
+ old: TransitionAnimation | TransitionAnimation[];
+ new: TransitionAnimation | TransitionAnimation[];
+}
+
+export interface TransitionDirectionalAnimations {
+ forwards: TransitionAnimationPair;
+ backwards: TransitionAnimationPair;
+}
+
+export type TransitionAnimationValue = 'morph' | 'slide' | 'fade' | TransitionDirectionalAnimations;
+
// Allow users to extend this for astro-jsx.d.ts
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AstroClientDirectives {}
@@ -69,6 +90,8 @@ export interface AstroBuiltinAttributes {
'set:html'?: any;
'set:text'?: any;
'is:raw'?: boolean;
+ 'transition:animate'?: 'morph' | 'slide' | 'fade' | TransitionDirectionalAnimations;
+ 'transition:name'?: string;
}
export interface AstroDefineVarsAttribute {
@@ -1227,6 +1250,27 @@ export interface AstroUserConfig {
* ```
*/
assets?: boolean;
+
+ /**
+ * @docs
+ * @name experimental.viewTransitions
+ * @type {boolean}
+ * @default `false`
+ * @version 2.9.0
+ * @description
+ * Enable experimental support for the `` component. With this enabled
+ * you can opt-in to [client-side routing](https://docs.astro.build/en/guides/client-side-routing/) on a per-page basis using this component
+ * and enable animations with the `transition:animate` directive.
+ *
+ * ```js
+ * {
+ * experimental: {
+ * viewTransitions: true,
+ * },
+ * }
+ * ```
+ */
+ viewTransitions?: boolean;
};
// Legacy options to be removed
diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts
index a0bc79c72df3..c1df9e5f3e62 100644
--- a/packages/astro/src/core/compile/compile.ts
+++ b/packages/astro/src/core/compile/compile.ts
@@ -45,6 +45,8 @@ export async function compile({
astroGlobalArgs: JSON.stringify(astroConfig.site),
scopedStyleStrategy: astroConfig.scopedStyleStrategy,
resultScopedSlot: true,
+ experimentalTransitions: astroConfig.experimental.viewTransitions,
+ transitionsAnimationURL: 'astro/components/viewtransitions.css',
preprocessStyle: createStylePreprocessor({
filename,
viteConfig,
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 7a27f1abea21..99c839a6dde7 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -45,6 +45,7 @@ const ASTRO_CONFIG_DEFAULTS = {
redirects: {},
experimental: {
assets: false,
+ viewTransitions: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };
@@ -232,6 +233,7 @@ export const AstroConfigSchema = z.object({
experimental: z
.object({
assets: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.assets),
+ viewTransitions: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.viewTransitions),
})
.passthrough()
.refine(
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index 52b5fd277a40..658648e3eefd 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -27,6 +27,7 @@ import astroScannerPlugin from '../vite-plugin-scanner/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { vitePluginSSRManifest } from '../vite-plugin-ssr-manifest/index.js';
+import astroTransitions from '../transitions/vite-plugin-transitions.js';
import { joinPaths } from './path.js';
interface CreateViteOptions {
@@ -132,6 +133,7 @@ export async function createVite(
astroContentAssetPropagationPlugin({ mode, settings }),
vitePluginSSRManifest(),
settings.config.experimental.assets ? [astroAssetsPlugin({ settings, logging, mode })] : [],
+ astroTransitions({ config: settings.config }),
],
publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root),
diff --git a/packages/astro/src/runtime/server/astro-component.ts b/packages/astro/src/runtime/server/astro-component.ts
index 44428b9295ad..aa205d790880 100644
--- a/packages/astro/src/runtime/server/astro-component.ts
+++ b/packages/astro/src/runtime/server/astro-component.ts
@@ -7,7 +7,7 @@ function validateArgs(args: unknown[]): args is Parameters) => {
if (!validateArgs(args)) {
@@ -22,6 +22,7 @@ function baseCreateComponent(cb: AstroComponentFactory, moduleId?: string): Astr
// Add a flag to this callback to mark it as an Astro component
fn.isAstroComponentFactory = true;
fn.moduleId = moduleId;
+ fn.propagation = propagation;
return fn;
}
@@ -32,17 +33,17 @@ interface CreateComponentOptions {
}
function createComponentWithOptions(opts: CreateComponentOptions) {
- const cb = baseCreateComponent(opts.factory, opts.moduleId);
- cb.propagation = opts.propagation;
+ const cb = baseCreateComponent(opts.factory, opts.moduleId, opts.propagation);
return cb;
}
// Used in creating the component. aka the main export.
export function createComponent(
arg1: AstroComponentFactory | CreateComponentOptions,
- moduleId?: string
+ moduleId?: string,
+ propagation?: PropagationHint,
) {
if (typeof arg1 === 'function') {
- return baseCreateComponent(arg1, moduleId);
+ return baseCreateComponent(arg1, moduleId, propagation);
} else {
return createComponentWithOptions(arg1);
}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 564ebbe91814..08a205e058ea 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -33,6 +33,7 @@ export {
stringifyChunk,
voidElementNames,
} from './render/index.js';
+export { renderTransition } from './transition.js';
export type {
AstroComponentFactory,
AstroComponentInstance,
diff --git a/packages/astro/src/runtime/server/transition.ts b/packages/astro/src/runtime/server/transition.ts
new file mode 100644
index 000000000000..98149e38a78b
--- /dev/null
+++ b/packages/astro/src/runtime/server/transition.ts
@@ -0,0 +1,155 @@
+import type {
+ SSRResult,
+ TransitionAnimation,
+ TransitionDirectionalAnimations,
+ TransitionAnimationValue,
+} from '../../@types/astro';
+import { markHTMLString } from './escape.js';
+import { slide, fade } from '../../transitions/index.js';
+
+const transitionNameMap = new WeakMap();
+function incrementTransitionNumber(result: SSRResult) {
+ let num = 1;
+ if(transitionNameMap.has(result)) {
+ num = transitionNameMap.get(result)! + 1;
+ }
+ transitionNameMap.set(result, num);
+ return num;
+}
+
+function createTransitionScope(result: SSRResult, hash: string) {
+ const num = incrementTransitionNumber(result);
+ return `astro-${hash}-${num}`;
+}
+export function renderTransition(result: SSRResult, hash: string, animationName: TransitionAnimationValue | undefined, transitionName: string) {
+ let animations: TransitionDirectionalAnimations | null = null;
+ switch(animationName) {
+ case 'fade': {
+ animations = fade();
+ break;
+ }
+ case 'slide': {
+ animations = slide();
+ break;
+ }
+ default: {
+ if(typeof animationName === 'object') {
+ animations = animationName;
+ }
+ }
+ }
+
+ const scope = createTransitionScope(result, hash);
+
+ // Default transition name is the scope of the element, ie HASH-1
+ if(!transitionName) {
+ transitionName = scope;
+ }
+
+ const styles = markHTMLString(``)
+
+ result._metadata.extraHead.push(styles);
+
+ return scope;
+}
+
+type AnimationBuilder = {
+ toString(): string;
+ [key: string]: string[] | ((k: string) => string);
+}
+
+function addAnimationProperty(builder: AnimationBuilder, prop: string, value: string | number) {
+ let arr = builder[prop];
+ if(Array.isArray(arr)) {
+ arr.push(value.toString());
+ } else {
+ builder[prop] = [value.toString()];
+ }
+}
+
+function animationBuilder(): AnimationBuilder {
+ return {
+ toString() {
+ let out = '';
+ for(let k in this) {
+ let value = this[k];
+ if(Array.isArray(value)) {
+ out += `\n\t${k}: ${value.join(', ')};`
+ }
+ }
+ return out;
+ }
+ };
+}
+
+function stringifyAnimation(anim: TransitionAnimation | TransitionAnimation[]): string {
+ if(Array.isArray(anim)) {
+ return stringifyAnimations(anim);
+ } else {
+ return stringifyAnimations([anim]);
+ }
+}
+
+function stringifyAnimations(anims: TransitionAnimation[]): string {
+ const builder = animationBuilder();
+
+ for(const anim of anims) {
+ /*300ms cubic-bezier(0.4, 0, 0.2, 1) both astroSlideFromRight;*/
+ if(anim.duration) {
+ addAnimationProperty(builder, 'animation-duration', toTimeValue(anim.duration));
+ }
+ if(anim.easing) {
+ addAnimationProperty(builder, 'animation-timing-function', anim.easing);
+ }
+ if(anim.direction) {
+ addAnimationProperty(builder, 'animation-direction', anim.direction);
+ }
+ if(anim.delay) {
+ addAnimationProperty(builder, 'animation-delay', anim.delay);
+ }
+ if(anim.fillMode) {
+ addAnimationProperty(builder, 'animation-fill-mode', anim.fillMode);
+ }
+ addAnimationProperty(builder, 'animation-name', anim.name);
+ }
+
+ return builder.toString();
+}
+
+function toTimeValue(num: number | string) {
+ return typeof num === 'number' ? num + 'ms' : num;
+}
diff --git a/packages/astro/src/transitions/index.ts b/packages/astro/src/transitions/index.ts
new file mode 100644
index 000000000000..d8dbb4e3bde7
--- /dev/null
+++ b/packages/astro/src/transitions/index.ts
@@ -0,0 +1,65 @@
+import type { TransitionDirectionalAnimations, TransitionAnimationPair } from '../@types/astro';
+
+export function slide({
+ duration
+}: {
+ duration?: string | number;
+} = {}): TransitionDirectionalAnimations {
+ return {
+ forwards: {
+ old: [{
+ name: 'astroFadeOut',
+ duration: duration ?? '90ms',
+ easing: 'cubic-bezier(0.4, 0, 1, 1)',
+ fillMode: 'both'
+ }, {
+ name: 'astroSlideToLeft',
+ duration: duration ?? '300ms',
+ easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ fillMode: 'both'
+ }],
+ new: [{
+ name: 'astroFadeIn',
+ duration: duration ?? '210ms',
+ easing: 'cubic-bezier(0, 0, 0.2, 1)',
+ delay: '90ms',
+ fillMode: 'both'
+ }, {
+ name: 'astroSlideFromRight',
+ duration: duration ?? '300ms',
+ easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ fillMode: 'both'
+ }]
+ },
+ backwards: {
+ old: [{ name: 'astroFadeOut' }, { name: 'astroSlideToRight' }],
+ new: [{ name: 'astroFadeIn' }, { name: 'astroSlideFromLeft' }]
+ }
+ };
+}
+
+export function fade({
+ duration
+}: {
+ duration?: string | number;
+} = {}): TransitionDirectionalAnimations {
+ const anim = {
+ old: {
+ name: 'astroFadeInOut',
+ duration: duration ?? '0.2s',
+ easing: 'linear',
+ fillMode: 'forwards',
+ },
+ new: {
+ name: 'astroFadeInOut',
+ duration: duration ?? '0.3s',
+ easing: 'linear',
+ fillMode: 'backwards',
+ }
+ } satisfies TransitionAnimationPair;
+
+ return {
+ forwards: anim,
+ backwards: anim,
+ };
+}
diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts
new file mode 100644
index 000000000000..891d5a22fb0b
--- /dev/null
+++ b/packages/astro/src/transitions/vite-plugin-transitions.ts
@@ -0,0 +1,39 @@
+import type { AstroConfig } from '../@types/astro';
+import * as vite from 'vite';
+import { AstroError } from '../core/errors/index.js';
+
+const virtualModuleId = 'astro:transitions';
+const resolvedVirtualModuleId = '\0' + virtualModuleId;
+
+// The virtual module for the astro:transitions namespace
+export default function astroTransitions({ config }: { config: AstroConfig; }): vite.Plugin {
+ return {
+ name: 'astro:transitions',
+ async resolveId(id) {
+ if (id === virtualModuleId) {
+ return resolvedVirtualModuleId;
+ }
+ },
+ load(id) {
+ if (id === resolvedVirtualModuleId) {
+ if(!config.experimental.viewTransitions) {
+ throw new AstroError({
+ title: 'Experimental View Transitions not enabled',
+ message: `View Transitions support is experimental. To enable update your config to include:
+
+export default defineConfig({
+ experimental: {
+ viewTransitions: true
+ }
+})`
+ });
+ }
+
+ return `
+ export * from "astro/transitions";
+ export { default as ViewTransitions } from "astro/components/ViewTransitions.astro";
+ `;
+ }
+ },
+ };
+}
diff --git a/packages/astro/test/units/compile/invalid-css.test.js b/packages/astro/test/units/compile/invalid-css.test.js
index d52b66c92ca0..db892b5bf309 100644
--- a/packages/astro/test/units/compile/invalid-css.test.js
+++ b/packages/astro/test/units/compile/invalid-css.test.js
@@ -12,6 +12,7 @@ describe('astro/src/core/compile', () => {
await cachedCompilation({
astroConfig: {
root: pathToFileURL('/'),
+ experimental: {}
},
viteConfig: await resolveConfig({ configFile: false }, 'serve'),
filename: '/src/pages/index.astro',
diff --git a/packages/astro/test/units/vite-plugin-astro/compile.test.js b/packages/astro/test/units/vite-plugin-astro/compile.test.js
index 02a88855ea15..5fa87433eb90 100644
--- a/packages/astro/test/units/vite-plugin-astro/compile.test.js
+++ b/packages/astro/test/units/vite-plugin-astro/compile.test.js
@@ -13,7 +13,7 @@ const viteConfig = await resolveConfig({ configFile: false }, 'serve');
async function compile(source, id) {
return await cachedFullCompilation({
compileProps: {
- astroConfig: { root: pathToFileURL('/'), base: '/' },
+ astroConfig: { root: pathToFileURL('/'), base: '/', experimental: {} },
viteConfig,
filename: id,
source,
diff --git a/packages/astro/tsconfig.json b/packages/astro/tsconfig.json
index 23ac0c78b62b..839239eafa22 100644
--- a/packages/astro/tsconfig.json
+++ b/packages/astro/tsconfig.json
@@ -6,6 +6,8 @@
"declarationDir": "./dist",
"module": "ES2022",
"outDir": "./dist",
- "target": "ES2021"
+ "target": "ES2021",
+ "jsx": "preserve",
+ "types": ["@types/dom-view-transitions", "network-information-types"]
}
}
diff --git a/packages/integrations/sitemap/test/fixtures/static/astro.config.mjs b/packages/integrations/sitemap/test/fixtures/static/astro.config.mjs
index e386f5e21d08..1b53c53a8ca1 100644
--- a/packages/integrations/sitemap/test/fixtures/static/astro.config.mjs
+++ b/packages/integrations/sitemap/test/fixtures/static/astro.config.mjs
@@ -7,7 +7,4 @@ export default defineConfig({
redirects: {
'/redirect': '/'
},
- experimental: {
- redirects: true
- }
})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cca772f05eeb..5781ec5b9ce6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -485,8 +485,8 @@ importers:
packages/astro:
dependencies:
'@astrojs/compiler':
- specifier: ^1.5.3
- version: 1.5.3
+ specifier: ^1.6.0
+ version: 1.6.0
'@astrojs/internal-helpers':
specifier: ^0.1.1
version: link:../internal-helpers
@@ -523,6 +523,9 @@ importers:
'@types/babel__core':
specifier: ^7.20.1
version: 7.20.1
+ '@types/dom-view-transitions':
+ specifier: ^1.0.1
+ version: 1.0.1
'@types/yargs-parser':
specifier: ^21.0.0
version: 21.0.0
@@ -592,6 +595,9 @@ importers:
mime:
specifier: ^3.0.0
version: 3.0.0
+ network-information-types:
+ specifier: ^0.1.1
+ version: 0.1.1(typescript@5.0.2)
ora:
specifier: ^6.3.1
version: 6.3.1
@@ -1478,6 +1484,12 @@ importers:
specifier: ^18.1.0
version: 18.2.0(react@18.2.0)
+ packages/astro/e2e/fixtures/view-transitions:
+ dependencies:
+ astro:
+ specifier: workspace:*
+ version: link:../../..
+
packages/astro/e2e/fixtures/vue-component:
dependencies:
'@astrojs/mdx':
@@ -5523,14 +5535,14 @@ packages:
sisteransi: 1.0.5
dev: false
- /@astrojs/compiler@1.5.3:
- resolution: {integrity: sha512-/HSFkJ+Yv+WUWSq0QVsIlhBKam5VUpGV+s8MvPguC/krHmw4Ww9TIgmfJSvV8/BN0sHJB7pCgf7yInae1zb+TQ==}
+ /@astrojs/compiler@1.6.0:
+ resolution: {integrity: sha512-vxuzp09jAW/ZQ8C4Itf6/OsF76TNjBQC06FNpcayKOzxYkCGHTLh7+0lF4ywmG/fDgSc+f1x7kKxxEKl4nqXvQ==}
/@astrojs/language-server@1.0.0:
resolution: {integrity: sha512-oEw7AwJmzjgy6HC9f5IdrphZ1GVgfV/+7xQuyf52cpTiRWd/tJISK3MsKP0cDkVlfodmNABNFnAaAWuLZEiiiA==}
hasBin: true
dependencies:
- '@astrojs/compiler': 1.5.3
+ '@astrojs/compiler': 1.6.0
'@jridgewell/trace-mapping': 0.3.18
'@vscode/emmet-helper': 2.8.8
events: 3.3.0
@@ -8580,6 +8592,10 @@ packages:
resolution: {integrity: sha512-OyiZ3jEKu7RtGO1yp9oOdK0cTwZ/10oE9PDJ6fyN3r9T5wkyOcvr6awdugjYdqF6KVO5eUvt7jx7rk2Eylufow==}
dev: true
+ /@types/dom-view-transitions@1.0.1:
+ resolution: {integrity: sha512-A9S1ijj/4MX06I1W/6on8lhaYyq1Ir7gaOvfllW1o4RzVWW88HAeqX0pUx9VgOLnNpdiGeUW2CTkg18p5LWIrA==}
+ dev: false
+
/@types/estree-jsx@1.0.0:
resolution: {integrity: sha512-3qvGd0z8F2ENTGr/GG1yViqfiKmRfrXVx5sJyHGFu3z7m5g5utCQtGp/g29JnjflhtQJBv1WDQukHiT58xPcYQ==}
dependencies:
@@ -14232,6 +14248,14 @@ packages:
engines: {node: '>= 0.4.0'}
dev: true
+ /network-information-types@0.1.1(typescript@5.0.2):
+ resolution: {integrity: sha512-mLXNafJYOkiJB6IlF727YWssTRpXitR+tKSLyA5VAdBi3SOvLf5gtizHgxf241YHPWocnAO/fAhVrB/68tPHDw==}
+ peerDependencies:
+ typescript: '>= 3.0.0'
+ dependencies:
+ typescript: 5.0.2
+ dev: false
+
/nice-try@1.0.5:
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
dev: true
@@ -15298,7 +15322,7 @@ packages:
resolution: {integrity: sha512-dPzop0gKZyVGpTDQmfy+e7FKXC9JT3mlpfYA2diOVz+Ui+QR1U4G/s+OesKl2Hib2JJOtAYJs/l+ovgT0ljlFA==}
engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'}
dependencies:
- '@astrojs/compiler': 1.5.3
+ '@astrojs/compiler': 1.6.0
prettier: 2.8.8
sass-formatter: 0.7.6
dev: true
@@ -15307,7 +15331,7 @@ packages:
resolution: {integrity: sha512-lJ/mG/Lz/ccSwNtwqpFS126mtMVzFVyYv0ddTF9wqwrEG4seECjKDAyw/oGv915rAcJi8jr89990nqfpmG+qdg==}
engines: {node: ^14.15.0 || >=16.0.0, pnpm: '>=7.14.0'}
dependencies:
- '@astrojs/compiler': 1.5.3
+ '@astrojs/compiler': 1.6.0
prettier: 2.8.8
sass-formatter: 0.7.6
synckit: 0.8.5