243 lines
5.1 KiB
Markdown
243 lines
5.1 KiB
Markdown
# CSS Animation Recipes
|
|
|
|
Ready-to-use CSS for `<ViewTransition>` props. Copy into your global stylesheet.
|
|
|
|
---
|
|
|
|
## Timing Variables
|
|
|
|
```css
|
|
:root {
|
|
--duration-exit: 150ms;
|
|
--duration-enter: 210ms;
|
|
--duration-move: 400ms;
|
|
}
|
|
```
|
|
|
|
### Shared Keyframes
|
|
|
|
```css
|
|
@keyframes fade {
|
|
from { filter: blur(3px); opacity: 0; }
|
|
to { filter: blur(0); opacity: 1; }
|
|
}
|
|
|
|
@keyframes slide {
|
|
from { translate: var(--slide-offset); }
|
|
to { translate: 0; }
|
|
}
|
|
|
|
@keyframes slide-y {
|
|
from { transform: translateY(var(--slide-y-offset, 10px)); }
|
|
to { transform: translateY(0); }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Fade
|
|
|
|
```css
|
|
::view-transition-old(.fade-out) {
|
|
animation: var(--duration-exit) ease-in fade reverse;
|
|
}
|
|
::view-transition-new(.fade-in) {
|
|
animation: var(--duration-enter) ease-out var(--duration-exit) both fade;
|
|
}
|
|
```
|
|
|
|
Usage: `<ViewTransition enter="fade-in" exit="fade-out" />`
|
|
|
|
---
|
|
|
|
## Slide (Vertical)
|
|
|
|
```css
|
|
::view-transition-old(.slide-down) {
|
|
animation:
|
|
var(--duration-exit) ease-out both fade reverse,
|
|
var(--duration-exit) ease-out both slide-y reverse;
|
|
}
|
|
::view-transition-new(.slide-up) {
|
|
animation:
|
|
var(--duration-enter) ease-in var(--duration-exit) both fade,
|
|
var(--duration-move) ease-in both slide-y;
|
|
}
|
|
```
|
|
|
|
Usage:
|
|
```jsx
|
|
<Suspense fallback={<ViewTransition exit="slide-down"><Skeleton /></ViewTransition>}>
|
|
<ViewTransition default="none" enter="slide-up"><Content /></ViewTransition>
|
|
</Suspense>
|
|
```
|
|
|
|
---
|
|
|
|
## Directional Navigation
|
|
|
|
### Separate Enter/Exit Classes
|
|
|
|
```css
|
|
::view-transition-new(.slide-from-right) {
|
|
--slide-offset: 60px;
|
|
animation:
|
|
var(--duration-enter) ease-out var(--duration-exit) both fade,
|
|
var(--duration-move) ease-in-out both slide;
|
|
}
|
|
::view-transition-old(.slide-to-left) {
|
|
--slide-offset: -60px;
|
|
animation:
|
|
var(--duration-exit) ease-in both fade reverse,
|
|
var(--duration-move) ease-in-out both slide reverse;
|
|
}
|
|
|
|
::view-transition-new(.slide-from-left) {
|
|
--slide-offset: -60px;
|
|
animation:
|
|
var(--duration-enter) ease-out var(--duration-exit) both fade,
|
|
var(--duration-move) ease-in-out both slide;
|
|
}
|
|
::view-transition-old(.slide-to-right) {
|
|
--slide-offset: 60px;
|
|
animation:
|
|
var(--duration-exit) ease-in both fade reverse,
|
|
var(--duration-move) ease-in-out both slide reverse;
|
|
}
|
|
```
|
|
|
|
### Single-Class Approach
|
|
|
|
```css
|
|
::view-transition-old(.nav-forward) {
|
|
--slide-offset: -60px;
|
|
animation:
|
|
var(--duration-exit) ease-in both fade reverse,
|
|
var(--duration-move) ease-in-out both slide reverse;
|
|
}
|
|
::view-transition-new(.nav-forward) {
|
|
--slide-offset: 60px;
|
|
animation:
|
|
var(--duration-enter) ease-out var(--duration-exit) both fade,
|
|
var(--duration-move) ease-in-out both slide;
|
|
}
|
|
|
|
::view-transition-old(.nav-back) {
|
|
--slide-offset: 60px;
|
|
animation:
|
|
var(--duration-exit) ease-in both fade reverse,
|
|
var(--duration-move) ease-in-out both slide reverse;
|
|
}
|
|
::view-transition-new(.nav-back) {
|
|
--slide-offset: -60px;
|
|
animation:
|
|
var(--duration-enter) ease-out var(--duration-exit) both fade,
|
|
var(--duration-move) ease-in-out both slide;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Shared Element Morph
|
|
|
|
```css
|
|
::view-transition-group(.morph) {
|
|
animation-duration: var(--duration-move);
|
|
}
|
|
|
|
::view-transition-image-pair(.morph) {
|
|
animation-name: via-blur;
|
|
}
|
|
|
|
@keyframes via-blur {
|
|
30% { filter: blur(3px); }
|
|
}
|
|
```
|
|
|
|
Usage: `<ViewTransition name={`product-${id}`} share="morph" />`
|
|
|
|
**Note:** Shared element transitions take raster snapshots. For text with significant size differences (e.g., `<h3>` → `<h1>`), the old snapshot gets scaled up, producing a visible ghost artifact. Use `text-morph` for text shared elements.
|
|
|
|
## Text Morph
|
|
|
|
Avoids raster scaling artifacts on text by hiding the old snapshot and showing the new text at full resolution:
|
|
|
|
```css
|
|
::view-transition-group(.text-morph) {
|
|
animation-duration: var(--duration-move);
|
|
}
|
|
::view-transition-old(.text-morph) {
|
|
display: none;
|
|
}
|
|
::view-transition-new(.text-morph) {
|
|
animation: none;
|
|
object-fit: none;
|
|
object-position: left top;
|
|
}
|
|
```
|
|
|
|
Usage: `<ViewTransition name={`title-${id}`} share="text-morph" />`
|
|
|
|
---
|
|
|
|
## Scale
|
|
|
|
```css
|
|
::view-transition-old(.scale-out) {
|
|
animation: var(--duration-exit) ease-in scale-down;
|
|
}
|
|
::view-transition-new(.scale-in) {
|
|
animation: var(--duration-enter) ease-out var(--duration-exit) both scale-up;
|
|
}
|
|
|
|
@keyframes scale-down {
|
|
from { transform: scale(1); opacity: 1; }
|
|
to { transform: scale(0.85); opacity: 0; }
|
|
}
|
|
@keyframes scale-up {
|
|
from { transform: scale(0.85); opacity: 0; }
|
|
to { transform: scale(1); opacity: 1; }
|
|
}
|
|
```
|
|
|
|
Usage: `<ViewTransition enter="scale-in" exit="scale-out" />`
|
|
|
|
---
|
|
|
|
## Persistent Element Isolation
|
|
|
|
```css
|
|
::view-transition-group(persistent-nav) {
|
|
animation: none;
|
|
z-index: 100;
|
|
}
|
|
```
|
|
|
|
### Backdrop-Blur Workaround
|
|
|
|
For elements with `backdrop-filter`, hide the old snapshot to avoid flash:
|
|
|
|
```css
|
|
::view-transition-old(persistent-nav) {
|
|
display: none;
|
|
}
|
|
::view-transition-new(persistent-nav) {
|
|
animation: none;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Reduced Motion
|
|
|
|
```css
|
|
@media (prefers-reduced-motion: reduce) {
|
|
::view-transition-old(*),
|
|
::view-transition-new(*),
|
|
::view-transition-group(*) {
|
|
animation-duration: 0s !important;
|
|
animation-delay: 0s !important;
|
|
}
|
|
}
|
|
```
|