واجهة View Transitions مع Next.js App Router: انتقالات سلسة بين الصفحات بدون مكتبات

لطالما كانت انتقالات الصفحات من أصعب الأمور في تطبيقات الويب. تتطلب الطرق التقليدية مكتبات حركة ثقيلة وإدارة حالة معقدة وتنسيقاً دقيقاً بين العناصر الداخلة والخارجة. تغير واجهة View Transitions API كل شيء بمنحها المتصفحات دعماً أصلياً لانتقالات سلسة بين حالات الصفحة.
في هذا الدرس التعليمي، ستبني تطبيق Next.js App Router مع انتقالات صفحات سلسة تماماً باستخدام واجهة View Transitions API وCSS فقط. بدون Framer Motion، بدون GSAP، بدون مكتبات حركة خارجية — فقط إمكانيات المنصة.
المتطلبات المسبقة
قبل البدء، تأكد من وجود:
- Node.js 18+ مثبت (يُفضل Node 20)
- npm أو pnpm كمدير حزم
- معرفة متوسطة بـ React وNext.js App Router
- فهم أساسي لـ حركات وانتقالات CSS
- متصفح حديث (Chrome 111+، Edge 111+، Safari 18+، Firefox 126+)
ما الذي ستبنيه
بنهاية هذا الدرس، سيكون لديك:
- تطبيق Next.js مع انتقالات تلاشي سلسة بين الصفحات
- حركات تحويل (morph) تربط العناصر المشتركة بسلاسة عبر المسارات
- انتقالات انزلاق اتجاهية مخصصة بناءً على اتجاه التنقل
- خطاف انتقال قابل لإعادة الاستخدام يمكنك إضافته لأي مشروع Next.js
فهم واجهة View Transitions API
تعمل واجهة View Transitions API عن طريق التقاط لقطة شاشة لحالة الصفحة الحالية، ثم تحديث DOM، ثم تحريك الانتقال بين الحالتين القديمة والجديدة. يتولى المتصفح كل العمل الثقيل — أنت فقط تخبره بما يجب الانتقال إليه.
إليك المفهوم الأساسي:
// الواجهة الأساسية
document.startViewTransition(() => {
// حدّث DOM هنا
updateThePage();
});عند استدعاء startViewTransition، يقوم المتصفح بما يلي:
- يلتقط الحالة المرئية الحالية كلقطة شاشة
- يشغل دالة الاستدعاء لتحديث DOM
- يلتقط الحالة المرئية الجديدة
- ينشئ عناصر زائفة (
::view-transition-oldو::view-transition-new) - يحرك الانتقال بين القديم والجديد باستخدام حركات CSS
السحر في أنك تتحكم في الحركة بالكامل من خلال CSS.
العناصر الزائفة لانتقالات العرض
ينشئ المتصفح شجرة من العناصر الزائفة أثناء الانتقال:
::view-transition
├── ::view-transition-group(root)
│ └── ::view-transition-image-pair(root)
│ ├── ::view-transition-old(root)
│ └── ::view-transition-new(root)
├── ::view-transition-group(header)
│ └── ::view-transition-image-pair(header)
│ ├── ::view-transition-old(header)
│ └── ::view-transition-new(header)
كل مجموعة انتقال مسماة تحصل على مجموعة خاصة بها من العناصر الزائفة، يمكنك تحريك كل منها بشكل مستقل.
الخطوة 1: إعداد المشروع
أنشئ مشروع Next.js جديد مع App Router:
npx create-next-app@latest view-transitions-demo \
--typescript \
--tailwind \
--app \
--src-dir \
--import-alias "@/*"
cd view-transitions-demoثبت الاعتمادية الإضافية الوحيدة — أداة صغيرة لأسماء الفئات الشرطية:
npm install clsxهذا كل شيء. لا مكتبات حركة.
الخطوة 2: تفعيل View Transitions في Next.js
لا يفعّل Next.js واجهة View Transitions افتراضياً. تحتاج للاشتراك من خلال ملف next.config.ts:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
viewTransition: true,
},
};
export default nextConfig;يخبر هذا العلم Next.js بلف انتقالات المسارات في document.startViewTransition() تلقائياً. بدونه، تحدث الانتقالات فورياً بدون حركة.
الخطوة 3: إنشاء التخطيط الأساسي
أعد تخطيطاً مع شريط تنقل يستمر عبر الصفحات:
// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import Link from "next/link";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "View Transitions Demo",
description: "انتقالات صفحات سلسة مع View Transitions API",
};
const navItems = [
{ href: "/", label: "الرئيسية" },
{ href: "/about", label: "حول" },
{ href: "/projects", label: "المشاريع" },
{ href: "/blog", label: "المدونة" },
];
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<nav className="sticky top-0 z-50 border-b border-gray-200 bg-white/80 backdrop-blur-sm">
<div className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
<Link
href="/"
className="text-xl font-bold text-gray-900"
style={{ viewTransitionName: "site-logo" }}
>
VT Demo
</Link>
<ul className="flex gap-6">
{navItems.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className="text-gray-600 transition-colors hover:text-gray-900"
>
{item.label}
</Link>
</li>
))}
</ul>
</div>
</nav>
<main className="mx-auto max-w-5xl px-6 py-12">{children}</main>
</body>
</html>
);
}لاحظ viewTransitionName على الشعار. هذه هي خاصية CSS الأساسية التي تخبر المتصفح بتحويل هذا العنصر بين الصفحات بدلاً من تلاشي الصفحة بالكامل.
الخطوة 4: إضافة أنماط View Transition العامة
أضف أنماط الانتقال الأساسية إلى CSS العام:
/* src/app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* انتقال تلاشي افتراضي للصفحة بأكملها */
::view-transition-old(root) {
animation: fade-out 0.3s ease-in-out;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in-out;
}
@keyframes fade-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.98);
}
}
@keyframes fade-in {
from {
opacity: 0;
transform: scale(1.02);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* حركة تحويل للعناصر المسماة (مثل الشعار) */
::view-transition-group(site-logo) {
animation-duration: 0.3s;
animation-timing-function: ease-in-out;
}
/* منع تحول التخطيط أثناء الانتقالات */
::view-transition {
pointer-events: none;
}بهذا الـ CSS فقط، كل تنقل بين الصفحات أصبح يحتوي على تلاشي سلس مع تأثير تكبير بسيط. الشعار يتحول بسلاسة لأنه يملك viewTransitionName.
الخطوة 5: بناء صفحات مع عناصر مشتركة
أنشئ الصفحة الرئيسية مع بطاقات مشاريع ستتحول إلى صفحات تفاصيلها:
// src/app/page.tsx
import Link from "next/link";
const projects = [
{
id: "aurora",
title: "Aurora",
description: "منصة تعاون في الوقت الفعلي مبنية باستخدام WebSockets",
color: "bg-gradient-to-br from-purple-500 to-pink-500",
tags: ["React", "WebSocket", "Redis"],
},
{
id: "nebula",
title: "Nebula",
description: "خط أنابيب نشر سحابي مع إصدارات بدون توقف",
color: "bg-gradient-to-br from-blue-500 to-cyan-500",
tags: ["Docker", "Kubernetes", "Go"],
},
{
id: "prism",
title: "Prism",
description: "أداة مراجعة كود مدعومة بالذكاء الاصطناعي تكتشف الأخطاء قبل شحنها",
color: "bg-gradient-to-br from-amber-500 to-orange-500",
tags: ["Python", "ML", "TypeScript"],
},
];
export default function HomePage() {
return (
<div>
<h1
className="mb-4 text-5xl font-bold tracking-tight text-gray-900"
style={{ viewTransitionName: "page-title" }}
>
مرحباً
</h1>
<p className="mb-12 text-xl text-gray-600">
استكشف أحدث مشاريعنا مع انتقالات سلسة.
</p>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<Link
key={project.id}
href={`/projects/${project.id}`}
className="group block"
>
<div
className={`${project.color} mb-4 h-48 rounded-2xl transition-shadow group-hover:shadow-xl`}
style={{
viewTransitionName: `project-image-${project.id}`,
}}
/>
<h2
className="mb-2 text-xl font-semibold text-gray-900"
style={{
viewTransitionName: `project-title-${project.id}`,
}}
>
{project.title}
</h2>
<p className="text-gray-600">{project.description}</p>
<div className="mt-3 flex gap-2">
{project.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700"
>
{tag}
</span>
))}
</div>
</Link>
))}
</div>
</div>
);
}كل بطاقة مشروع لها قيم viewTransitionName فريدة لصورتها وعنوانها. يجب أن تتطابق هذه الأسماء بالضبط في صفحة التفاصيل ليعمل التحويل.
الخطوة 6: إنشاء صفحة تفاصيل المشروع مع انتقالات التحويل
// src/app/projects/[id]/page.tsx
import Link from "next/link";
import { notFound } from "next/navigation";
const projects: Record<
string,
{
title: string;
description: string;
color: string;
tags: string[];
content: string;
}
> = {
aurora: {
title: "Aurora",
description: "منصة تعاون في الوقت الفعلي مبنية باستخدام WebSockets",
color: "bg-gradient-to-br from-purple-500 to-pink-500",
tags: ["React", "WebSocket", "Redis"],
content:
"تمكّن Aurora الفرق من التعاون في الوقت الفعلي بزمن استجابة أقل من 50 مللي ثانية. مبنية على بنية WebSocket مع Redis pub/sub للتوسع الأفقي، تدعم المؤشرات الحية والتحرير المتزامن والوعي بالحضور عبر الفرق الموزعة.",
},
nebula: {
title: "Nebula",
description: "خط أنابيب نشر سحابي مع إصدارات بدون توقف",
color: "bg-gradient-to-br from-blue-500 to-cyan-500",
tags: ["Docker", "Kubernetes", "Go"],
content:
"يؤتمت Nebula دورة النشر بالكامل من دفع الكود إلى الإنتاج. مع نشر كناري مدمج وتراجع تلقائي وفحوصات صحة شاملة، يضمن بقاء خدماتك متاحة خلال كل إصدار.",
},
prism: {
title: "Prism",
description: "أداة مراجعة كود مدعومة بالذكاء الاصطناعي تكتشف الأخطاء قبل شحنها",
color: "bg-gradient-to-br from-amber-500 to-orange-500",
tags: ["Python", "ML", "TypeScript"],
content:
"يستخدم Prism نماذج تعلم آلي مدربة على ملايين مراجعات الكود لتحديد الأخطاء المحتملة والثغرات الأمنية ومشاكل الأداء قبل وصولها للإنتاج. يتكامل مباشرة في خط أنابيب CI الخاص بك.",
},
};
export default async function ProjectPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const project = projects[id];
if (!project) notFound();
return (
<div>
<Link
href="/"
className="mb-8 inline-flex items-center text-gray-500 hover:text-gray-900"
>
العودة للمشاريع ←
</Link>
<div
className={`${project.color} mb-8 h-64 rounded-2xl md:h-80`}
style={{ viewTransitionName: `project-image-${id}` }}
/>
<h1
className="mb-4 text-4xl font-bold text-gray-900"
style={{ viewTransitionName: `project-title-${id}` }}
>
{project.title}
</h1>
<div className="mb-6 flex gap-2">
{project.tags.map((tag) => (
<span
key={tag}
className="rounded-full bg-gray-100 px-4 py-1.5 text-sm font-medium text-gray-700"
>
{tag}
</span>
))}
</div>
<p className="text-lg leading-relaxed text-gray-700">
{project.content}
</p>
</div>
);
}عند النقر على بطاقة مشروع في الصفحة الرئيسية، يتحول صندوق التدرج اللوني بسلاسة من حجم البطاقة إلى عرض البطل الكامل. ينزلق العنوان إلى موضعه الجديد. كل شيء آخر يتلاشى. كل هذا يحدث بدون أي كود JavaScript للحركة.
الخطوة 7: بناء خطاف انتقال مخصص
لمزيد من التحكم في الانتقالات، أنشئ خطافاً مخصصاً يتيح لك تشغيل الانتقالات برمجياً:
// src/hooks/use-view-transition.ts
"use client";
import { useCallback, useRef } from "react";
type TransitionCallback = () => void | Promise<void>;
interface ViewTransitionOptions {
onStart?: () => void;
onFinish?: () => void;
onError?: (error: unknown) => void;
}
export function useViewTransition(options: ViewTransitionOptions = {}) {
const transitionRef = useRef<ViewTransition | null>(null);
const startTransition = useCallback(
async (callback: TransitionCallback) => {
// بديل للمتصفحات التي لا تدعم View Transitions
if (!document.startViewTransition) {
await callback();
return;
}
options.onStart?.();
try {
const transition = document.startViewTransition(async () => {
await callback();
});
transitionRef.current = transition;
await transition.finished;
options.onFinish?.();
} catch (error) {
options.onError?.(error);
} finally {
transitionRef.current = null;
}
},
[options]
);
const skipTransition = useCallback(() => {
transitionRef.current?.skipTransition();
}, []);
return { startTransition, skipTransition };
}استخدم هذا الخطاف لتغييرات الحالة من جانب العميل التي يجب أن تتحرك:
// مثال: مبدل علامات تبويب متحرك
"use client";
import { useState } from "react";
import { useViewTransition } from "@/hooks/use-view-transition";
export function AnimatedTabs() {
const [activeTab, setActiveTab] = useState(0);
const { startTransition } = useViewTransition();
const tabs = ["نظرة عامة", "المميزات", "الأسعار"];
const switchTab = (index: number) => {
startTransition(() => {
setActiveTab(index);
});
};
return (
<div>
<div className="flex gap-1 rounded-lg bg-gray-100 p-1">
{tabs.map((tab, i) => (
<button
key={tab}
onClick={() => switchTab(i)}
className="relative rounded-md px-4 py-2 text-sm font-medium"
>
{activeTab === i && (
<span
className="absolute inset-0 rounded-md bg-white shadow-sm"
style={{ viewTransitionName: "active-tab" }}
/>
)}
<span className="relative z-10">{tab}</span>
</button>
))}
</div>
<div
className="mt-6"
style={{ viewTransitionName: "tab-content" }}
>
<p>محتوى {tabs[activeTab]}</p>
</div>
</div>
);
}مؤشر علامة التبويب النشطة يتحول بسلاسة بين المواضع، والمحتوى يتلاشى — كل ذلك مدفوع بـ CSS.
الخطوة 8: انتقالات انزلاق اتجاهية
أحد الأنماط الشائعة هو انزلاق الصفحات في اتجاهات مختلفة بناءً على التنقل. يمكنك تحقيق ذلك بإضافة فئات CSS تتحكم في اتجاه الحركة:
/* src/app/globals.css — أضف هذه */
/* انتقال انزلاق لليسار (التنقل للأمام) */
.slide-forward::view-transition-old(root) {
animation: slide-out-left 0.3s ease-in-out;
}
.slide-forward::view-transition-new(root) {
animation: slide-in-right 0.3s ease-in-out;
}
/* انتقال انزلاق لليمين (التنقل للخلف) */
.slide-back::view-transition-old(root) {
animation: slide-out-right 0.3s ease-in-out;
}
.slide-back::view-transition-new(root) {
animation: slide-in-left 0.3s ease-in-out;
}
@keyframes slide-out-left {
to {
opacity: 0;
transform: translateX(-30px);
}
}
@keyframes slide-in-right {
from {
opacity: 0;
transform: translateX(30px);
}
}
@keyframes slide-out-right {
to {
opacity: 0;
transform: translateX(30px);
}
}
@keyframes slide-in-left {
from {
opacity: 0;
transform: translateX(-30px);
}
}أنشئ مكون رابط يحدد فئة الاتجاه:
// src/components/directional-link.tsx
"use client";
import Link from "next/link";
import { type ComponentProps, useCallback } from "react";
type Direction = "forward" | "back";
interface DirectionalLinkProps extends ComponentProps<typeof Link> {
direction?: Direction;
}
export function DirectionalLink({
direction = "forward",
onClick,
...props
}: DirectionalLinkProps) {
const handleClick = useCallback(
(e: React.MouseEvent<HTMLAnchorElement>) => {
document.documentElement.classList.remove(
"slide-forward",
"slide-back"
);
document.documentElement.classList.add(
direction === "forward" ? "slide-forward" : "slide-back"
);
onClick?.(e);
},
[direction, onClick]
);
return <Link {...props} onClick={handleClick} />;
}الآن استخدم DirectionalLink في صفحاتك:
// على زر العودة في صفحة التفاصيل
<DirectionalLink href="/" direction="back">
العودة للمشاريع ←
</DirectionalLink>
// على بطاقات المشاريع في الصفحة الرئيسية
<DirectionalLink href={`/projects/${project.id}`} direction="forward">
عرض المشروع →
</DirectionalLink>الخطوة 9: مجموعات الانتقال للحركات المستقلة
أحياناً تريد أن تتحرك أجزاء مختلفة من الصفحة بشكل مستقل. أسماء View Transition تتيح لك إنشاء مجموعات حركة منفصلة:
// src/app/blog/page.tsx
const posts = [
{ id: 1, title: "البدء مع Rust", date: "2026-03-15" },
{ id: 2, title: "لماذا TypeScript 6 مهم", date: "2026-03-10" },
{ id: 3, title: "الحوسبة الطرفية في 2026", date: "2026-03-05" },
];
export default function BlogPage() {
return (
<div>
<h1
className="mb-8 text-4xl font-bold"
style={{ viewTransitionName: "page-title" }}
>
المدونة
</h1>
<aside
className="mb-8 rounded-xl bg-blue-50 p-6"
style={{ viewTransitionName: "blog-sidebar" }}
>
<p className="text-blue-800">اشترك للحصول على مقالات جديدة.</p>
</aside>
<div className="space-y-6">
{posts.map((post) => (
<article
key={post.id}
className="rounded-xl border p-6"
style={{
viewTransitionName: `blog-post-${post.id}`,
}}
>
<time className="text-sm text-gray-500">{post.date}</time>
<h2 className="mt-1 text-xl font-semibold">{post.title}</h2>
</article>
))}
</div>
</div>
);
}أضف حركات متتابعة لمقالات المدونة:
/* src/app/globals.css — أضف هذه */
/* دخول متتابع لمقالات المدونة */
::view-transition-new(blog-post-1) {
animation: fade-in 0.3s ease-out 0.05s both;
}
::view-transition-new(blog-post-2) {
animation: fade-in 0.3s ease-out 0.1s both;
}
::view-transition-new(blog-post-3) {
animation: fade-in 0.3s ease-out 0.15s both;
}
/* الشريط الجانبي ينزلق من الجانب */
::view-transition-new(blog-sidebar) {
animation: slide-in-left 0.4s ease-out;
}
::view-transition-old(blog-sidebar) {
animation: slide-out-left 0.3s ease-in;
}كل عنصر مسمى ينتقل بشكل مستقل — الشريط الجانبي ينزلق، المقالات تدخل بالتتابع، وعنوان الصفحة يتحول. كل شيء تصريحي بـ CSS، بدون منطق حركة JavaScript.
الخطوة 10: التعامل مع تفضيلات تقليل الحركة
احترم دائماً المستخدمين الذين يفضلون تقليل الحركة. تجعل واجهة View Transitions API هذا سهلاً:
/* src/app/globals.css — أضف في الأعلى */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}هذه القاعدة الواحدة تعطل جميع حركات انتقال العرض للمستخدمين الذين فعّلوا "تقليل الحركة" في إعدادات نظام التشغيل. تحديث DOM يحدث كما هو — فقط الحركة تُزال.
يجب أيضاً التعامل مع هذا في خطافك المخصص:
// use-view-transition.ts المحدث
export function useViewTransition(options: ViewTransitionOptions = {}) {
const prefersReducedMotion =
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const startTransition = useCallback(
async (callback: TransitionCallback) => {
if (!document.startViewTransition || prefersReducedMotion) {
await callback();
return;
}
// ... بقية التنفيذ
},
[options, prefersReducedMotion]
);
// ...
}الخطوة 11: حركات تحويل متقدمة لمعارض الصور
أحد أكثر استخدامات View Transitions إثارة هو معارض الصور حيث تتحول الصور المصغرة إلى عرض بالحجم الكامل:
// src/app/about/page.tsx
const team = [
{ id: "sarah", name: "سارة تشن", role: "قائدة الهندسة" },
{ id: "omar", name: "عمر خليل", role: "مدير التصميم" },
{ id: "alex", name: "أليكس ريفيرا", role: "مدير المنتج" },
];
export default function AboutPage() {
return (
<div>
<h1
className="mb-4 text-4xl font-bold"
style={{ viewTransitionName: "page-title" }}
>
حول
</h1>
<p className="mb-12 text-lg text-gray-600">تعرف على الفريق وراء كل شيء.</p>
<div className="grid gap-8 md:grid-cols-3">
{team.map((member) => (
<div key={member.id} className="text-center">
<div
className="mx-auto mb-4 h-32 w-32 rounded-full bg-gradient-to-br from-gray-300 to-gray-400"
style={{
viewTransitionName: `avatar-${member.id}`,
}}
/>
<h3
className="font-semibold"
style={{
viewTransitionName: `name-${member.id}`,
}}
>
{member.name}
</h3>
<p className="text-sm text-gray-500">{member.role}</p>
</div>
))}
</div>
</div>
);
}أضف CSS لتحويل الصور الرمزية بسلاسة:
/* تحويل سلس للصور الرمزية */
::view-transition-group(avatar-sarah),
::view-transition-group(avatar-omar),
::view-transition-group(avatar-alex) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}عند التنقل بين الصفحات التي تشترك في نفس viewTransitionName، يحسب المتصفح فرق الموضع والحجم ويحرك الانتقال بينهما بسلاسة.
الخطوة 12: الاختبار وتصحيح الأخطاء
أدوات مطور Chrome
تحتوي أدوات مطور Chrome على دعم مدمج لتصحيح أخطاء View Transitions:
- افتح أدوات المطور واذهب إلى لوحة Animations
- تنقل بين الصفحات — يظهر كل انتقال في الجدول الزمني
- يمكنك إبطاء وإعادة تشغيل وفحص الحركات الفردية
- تعرض لوحة Elements العناصر الزائفة
::view-transitionأثناء الانتقال
إبطاء الانتقالات أثناء التطوير
أضف قاعدة CSS مؤقتة لإبطاء كل شيء أثناء التطوير:
/* للتطوير فقط — احذف قبل الإنتاج */
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 2s !important;
}مشاكل شائعة في التصحيح
المشكلة: العناصر تومض بدلاً من التحول
يحدث هذا عندما لا تتطابق قيم viewTransitionName بالضبط بين الصفحتين القديمة والجديدة.
المشكلة: عناصر متعددة بنفس اسم الانتقال
كل viewTransitionName يجب أن يكون فريداً في الصفحة في أي وقت. إذا شارك عنصران في نفس الاسم، ينقطع الانتقال بصمت:
// هذا سيفشل — عنصران بنفس الاسم
{items.map((item) => (
<div style={{ viewTransitionName: "card" }}>
{item.title}
</div>
))}
// هذا يعمل — أسماء فريدة
{items.map((item) => (
<div style={{ viewTransitionName: `card-${item.id}` }}>
{item.title}
</div>
))}المشكلة: الانتقالات لا تعمل
تأكد أن علم experimental.viewTransition مضبوط في next.config.ts. تأكد أيضاً أنك تستخدم next/link للتنقل — إعادة تحميل الصفحة الكاملة تتجاوز View Transitions.
اعتبارات الأداء
واجهة View Transitions عالية الأداء بشكل ملحوظ لأن:
- مسرّعة بالـ GPU: يركب المتصفح الحركة على GPU
- بدون اهتزاز التخطيط: تعمل الحركات على لقطات، ليس DOM حي
- تحسين تلقائي: يلتقط المتصفح فقط ما يحتاجه
لكن ضع هذه النصائح في اعتبارك:
- حدد الانتقالات المسماة: كل عنصر مسمى يضيف خطوة التقاط. اجعلها أقل من 10-15 عنصراً مسمى لكل صفحة
- تجنب الصور الكبيرة: يلتقط المتصفح العناصر بحجمها المعروض
- استخدم
will-changeباعتدال: المتصفح يحسن View Transitions تلقائياً - اجعل مدد الانتقال قصيرة: 200-400 مللي ثانية تبدو سريعة الاستجابة
دعم المتصفحات والتحسين التدريجي
اعتباراً من 2026، View Transitions مدعومة في:
| المتصفح | الإصدار | الدعم |
|---|---|---|
| Chrome | 111+ | كامل |
| Edge | 111+ | كامل |
| Safari | 18+ | كامل |
| Firefox | 126+ | كامل |
للمتصفحات الأقدم، الانتقال ببساطة لا يُشغل — التنقل يحدث فورياً. تطبيقك يعمل بشكل مثالي في كلتا الحالتين. هذا تحسين تدريجي حقيقي.
استكشاف الأخطاء
انقطاع الانتقال
إذا نقر المستخدم على رابط أثناء انتقال لا يزال يعمل، يتم تخطي الانتقال الحالي ويبدأ الجديد. هذا هو سلوك المتصفح الصحيح.
مكونات الخادم وView Transitions
تعمل View Transitions مع مكونات الخادم والعميل في Next.js. يمكن تعيين خاصية CSS viewTransitionName مضمنة على أي مكون. خطاف useViewTransition يعمل فقط في مكونات العميل، لكنك نادراً ما تحتاجه للانتقالات المبنية على المسارات.
أسماء الانتقال الديناميكية
عند إنشاء viewTransitionName ديناميكياً، تأكد أن القيمة هي CSS custom-ident صالح. تجنب الأسماء التي تبدأ بأرقام:
// سيئ — يبدأ برقم
style={{ viewTransitionName: `123-card` }}
// جيد — يبدأ بحرف
style={{ viewTransitionName: `card-123` }}الخطوات التالية
الآن بعد أن أصبح لديك انتقالات صفحات سلسة، إليك طرق للمضي قدماً:
- دمج مع Server Actions: استخدم View Transitions لتحديثات واجهة المستخدم التفاؤلية عند إرسال النماذج
- بناء حركة تخطيط مشترك: أنشئ شريطاً جانبياً دائماً يحول مؤشره النشط
- إضافة انتقالات خاصة بالصفحة: استخدم CSS
view-transition-nameعلىbodyمع فئات مختلفة لكل مسار - استكشاف الانتقالات عبر المستندات: المواصفات المستوى 2 القادمة تدعم الانتقالات عبر تنقلات الصفحة الكاملة (MPA)
الخلاصة
تلغي واجهة View Transitions API الحاجة لمكتبات حركة معقدة في معظم سيناريوهات التنقل. مع تكامل Next.js App Router، تحصل على تلاشي سلس تلقائياً، ومع بضع تصريحات viewTransitionName، تحصل على حركات تحويل مذهلة تبدو أصلية.
النقاط الرئيسية:
- فعّل
experimental.viewTransitionفيnext.config.tsلدعم انتقال المسارات التلقائي - استخدم خاصية CSS
viewTransitionNameلإنشاء حركات تحويل بين العناصر المشتركة - تحكم في الحركات بالكامل باستخدام العناصر الزائفة CSS — لا حاجة لـ JavaScript
- احترم دائماً
prefers-reduced-motionلإمكانية الوصول - View Transitions هي تحسين تدريجي — التطبيقات تعمل بشكل مثالي بدونها
أفضل مكتبة حركة هي تلك التي لا تحتاج لتثبيتها.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

Motion لـ React: بناء رسوم متحركة وإيماءات وانتقالات بمستوى إنتاجي
أتقن مكتبة Motion للرسوم المتحركة (المعروفة سابقاً بـ Framer Motion) لـ React. تعلم بناء رسوم متحركة سلسة وإيماءات تفاعلية وانتقالات التخطيط وتأثيرات التمرير ورسوم الخروج المتحركة مع TypeScript.

Zustand + Next.js App Router: إدارة حالة React الحديثة من الصفر إلى الإنتاج
أتقن إدارة حالة React الحديثة مع Zustand و Next.js 15 App Router. يغطي هذا الدليل العملي إنشاء المتاجر والوسائط والاستمرارية والترطيب من جانب الخادم وأنماط التطبيقات القابلة للتوسع.

بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15
تعلّم كيفية بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15. يغطي هذا الدليل تصميم المخططات والاستعلامات والتعديلات والاشتراكات الفورية والمصادقة ورفع الملفات — مع أمان أنواع شامل.