feat: section open/close animations via ViewTransition and @starting-style
Enable experimental.viewTransition in Next.js config. Wrap active section in ViewTransitionWrapper so the browser cross-fades between sections on navigation. Replace animate-fadeIn keyframe with @starting-style + CSS transition for the initial render enter animation.
This commit is contained in:
@@ -8,6 +8,9 @@ const isExport = process.env.STATIC_EXPORT === 'true';
|
|||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
...(isExport ? { output: 'export' } : {}),
|
...(isExport ? { output: 'export' } : {}),
|
||||||
images: { unoptimized: true },
|
images: { unoptimized: true },
|
||||||
|
experimental: {
|
||||||
|
viewTransition: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ describe('SectionAccordion', () => {
|
|||||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('content wrapper has animate-fadeIn class', () => {
|
it('content wrapper has section-content class', () => {
|
||||||
const { container } = render(<SectionAccordion {...activeProps} />);
|
const { container } = render(<SectionAccordion {...activeProps} />);
|
||||||
expect(container.querySelector('.animate-fadeIn')).toBeInTheDocument();
|
expect(container.querySelector('.section-content')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { ViewTransitionWrapper } from '$shared/ui';
|
||||||
|
|
||||||
interface SectionAccordionProps {
|
interface SectionAccordionProps {
|
||||||
/**
|
/**
|
||||||
@@ -35,17 +36,19 @@ export function SectionAccordion({ number, title, id, isActive, href, children }
|
|||||||
return (
|
return (
|
||||||
<section id={id} className="scroll-mt-8">
|
<section id={id} className="scroll-mt-8">
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<div className="mb-12">
|
<ViewTransitionWrapper name="section-content">
|
||||||
<div className="mb-16">
|
<div className="mb-12">
|
||||||
<h1
|
<div className="mb-16">
|
||||||
className="font-heading font-black text-5xl leading-[1.2] mb-0"
|
<h1
|
||||||
style={{ fontVariationSettings: "'WONK' 1, 'SOFT' 0" }}
|
className="font-heading font-black text-5xl leading-[1.2] mb-0"
|
||||||
>
|
style={{ fontVariationSettings: "'WONK' 1, 'SOFT' 0" }}
|
||||||
{number}. {title}
|
>
|
||||||
</h1>
|
{number}. {title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="section-content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="animate-fadeIn">{children}</div>
|
</ViewTransitionWrapper>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
|
|||||||
@@ -230,17 +230,49 @@
|
|||||||
border-right: var(--border-width) solid var(--blue);
|
border-right: var(--border-width) solid var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
/* Section content enter animation (initial render, no navigation) */
|
||||||
@keyframes fadeIn {
|
.section-content {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
transition:
|
||||||
|
opacity 0.35s ease,
|
||||||
|
transform 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
.section-content {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cross-section view transition (navigation between sections) */
|
||||||
|
::view-transition-old(section-content) {
|
||||||
|
animation: 200ms ease both section-fade-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(section-content) {
|
||||||
|
animation: 300ms ease both section-fade-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes section-fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes section-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(12px);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animate-fadeIn {
|
|
||||||
animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user