{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"vue-js-vs-next-js-modal-routing-a-side-by-side-breakdown-pn0kq","url":"https://zyvop.com/vue-js-vs-next-js-modal-routing-a-side-by-side-breakdown-pn0kq","title":"Vue.js vs Next.js: Modal Routing — A Side-by-Side Breakdown","subtitle":null,"tldr":"Let's look at the breakdown of both frameworks side-by-side to understand the massive shift in mindset. Project 1: The Vue.js Approach (Code-Driven Logic) Step ...","keywords":["webdev","Vue.js","Tutorial"],"entities":["Heba Allah Hashim","Software Engineer","webdev","Vue.js","Tutorial","ZyVOP"],"keyTakeaways":["Project 1: The Vue.js Approach (Code-Driven Logic) Step 1: Initialize the Project npm create vite@latest vue-modal-app -- --template vue cd vue-modal-app npm install vue-router@4 Step 2: Configure the Routing Paths (src/router.js) import { createRouter, createWebHistory } from 'vue-router' import GalleryView from './views/GalleryView.vue' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: GalleryView, children: [ { path: 'photo/:id', component: () =&gt; import('./components/PhotoModal.vue'), props: true } ] } ] }) Step 3: Link the Router to the Application Update src/main.js: import { createApp } from 'vue' import App from './App.vue' import { router } from './router' createApp(App).use(router).mount('#app') Replace src/App.vue: &lt;template&gt; &lt;router-view /&gt; &lt;/template&gt; Step 4: Create the Gallery List Page (src/views/GalleryView.vue) &lt;template&gt; &lt;div style=\"padding: 20px; font-family: sans-serif;\"&gt; &lt;h1&gt;Vue Photo Gallery&lt;/h1&gt; &lt;div style=\"display: flex; gap: 20px;\"&gt; &lt;div v-for=\"photoId in ['sunset', 'ocean', 'mountains']\" :key=\"photoId\" @click=\"openPhoto(photoId)\" style=\"width: 150px; height: 100px; background: #3b82f6; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 8px;\" &gt; View {{ photoId }} &lt;/div&gt; &lt;/div&gt; &lt;!-- Renders our Child Popup component over the grid --&gt; &lt;router-view v-if=\"showModal\" @close=\"closeModal\" /&gt; &lt;/div&gt; &lt;/template&gt; &lt;script setup&gt; import { ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' const route = useRoute() const router = useRouter() const showModal = ref(false) // Crucial: The Watcher forces the modal open if an ID exists on hard refresh watch( () =&gt; route.params.id, (newId) =&gt; { showModal.value = !!newId }, { immediate: true } ) function openPhoto(id) { router.push(`/photo/${id}`) } function closeModal() { router.push('/') } &lt;/script&gt; Step 5: Create the Popup Modal Component (src/components/PhotoModal.vue) &lt;template&gt; &lt;div style=\"position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;\"&gt; &lt;div style=\"background: white; padding: 30px; border-radius: 12px; text-align: center; width: 300px;\"&gt; &lt;h2&gt;Vue Modal Layout&lt;/h2&gt; &lt;p style=\"font-size: 24px; font-weight: bold; text-transform: uppercase; color: #3b82f6;\"&gt; 🌌 {{ id }} &lt;/p&gt; &lt;button @click=\"$emit('close')\" style=\"margin-top: 20px; padding: 8px 16px; cursor: pointer;\"&gt;Close Overlay&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script setup&gt; defineProps({ id: String }) defineEmits(['close']) &lt;/script&gt; Run via npm run dev to view.","Project 2: The Next.js Approach (Folder-Driven Architecture) Step 1: Initialize the Project # Configuration flags: TypeScript (Yes), ESLint (No), src/ (No), App Router (Yes) npx create-next-app@latest next-modal-app cd next-modal-app Step 2: Create the File Folders Manually create the exact folders matching the Hierarchy Map above directly inside your editor's file sidebar to prevent system escape errors.","Step 3: Set up the Main Gallery Page (app/gallery/page.tsx) import Link from 'next/link'; export default function GalleryPage() { return ( &lt;div style={{ padding: '20px', fontFamily: 'sans-serif' }}&gt; &lt;h1&gt;Next.js Photo Gallery&lt;/h1&gt; &lt;div style={{ display: 'flex', gap: '20px' }}&gt; {['sunset', 'ocean', 'mountains'].map((photoId) =&gt; ( &lt;Link key={photoId} href={`/gallery/photo/${photoId}`} style={{ width: '150px', height: '100px', background: '#ef4444', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', textDecoration: 'none', borderRadius: '8px' }} &gt; View {photoId} &lt;/Link&gt; ))} &lt;/div&gt; &lt;/div&gt; ); } Step 4: Configure the Layout Slots (app/gallery/layout.tsx) export default function GalleryLayout({ children, modal }: { children: React.ReactNode; modal: React.ReactNode; }) { return ( &lt;div&gt; {children} {modal} &lt;/div&gt; ); } Step 5: Create the Slot Fallback (app/gallery/@modal/default.tsx) export default function DefaultModal() { // Fixes the unmounted slot mismatch crash by returning a baseline null return null; } Step 6: Create the Intercepted Popup View (app/gallery/@modal/(.)photo/[id]/page.tsx) 'use client'; import { useRouter, useParams } from 'next/navigation'; export default function InterceptedPhotoPopup() { const router = useRouter(); const params = useParams(); return ( &lt;div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}&gt; &lt;div style={{ background: 'white', padding: '30px', borderRadius: '12px', textAlign: 'center', width: '300px' }}&gt; &lt;h2&gt;Next.js Intercepted Popup&lt;/h2&gt; &lt;p style={{ fontSize: '24px', fontWeight: 'bold', textTransform: 'uppercase', color: '#ef4444' }}&gt; 📸 {params.id} &lt;/p&gt; &lt;button onClick={() =&gt; router.back()} style={{ marginTop: '20px', padding: '8px 16px', cursor: 'pointer' }}&gt; Close Overlay &lt;/button&gt; &lt;/div&gt; &lt;/div&gt; ); } Step 7: Create the Standalone Fallback Page (app/gallery/photo/[id]/page.tsx) import Link from 'next/link'; interface PageProps { params: Promise&lt;{ id: string }&gt;; } export default async function StandalonePhotoPage({ params }: PageProps) { // Modern Next.js rule: params must be unwrapped asynchronously const { id } = await params; return ( &lt;div style={{ padding: '40px', fontFamily: 'sans-serif', textAlign: 'center' }}&gt; &lt;h1&gt;Standalone Full-Screen View&lt;/h1&gt; &lt;div style={{ margin: '40px auto', width: '400px', height: '250px', background: '#ef4444', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '32px', borderRadius: '12px' }}&gt; 🌌 {id.toUpperCase()} &lt;/div&gt; &lt;Link href=\"/gallery\" style={{ color: '#ef4444' }}&gt;← Back to Gallery List&lt;/Link&gt; &lt;/div&gt; ); } Run via npm run dev and navigate to http://localhost:3000/gallery."],"headings":["Project 1: The Vue.js Approach (Code-Driven Logic)","Step 1: Initialize the Project","Step 2: Configure the Routing Paths (src/router.js)","Step 3: Link the Router to the Application","Step 4: Create the Gallery List Page (src/views/GalleryView.vue)","Step 5: Create the Popup Modal Component (src/components/PhotoModal.vue)","Project 2: The Next.js Approach (Folder-Driven Architecture)","Step 2: Create the File Folders","Step 3: Set up the Main Gallery Page (app/gallery/page.tsx)","Step 4: Configure the Layout Slots (app/gallery/layout.tsx)","Step 5: Create the Slot Fallback (app/gallery/@modal/default.tsx)","Step 6: Create the Intercepted Popup View (app/gallery/@modal/(.)photo/[id]/page.tsx)","Step 7: Create the Standalone Fallback Page (app/gallery/photo/[id]/page.tsx)"],"outboundLinks":[],"contentText":"Let's look at the breakdown of both frameworks side-by-side to understand the massive shift in mindset. Project 1: The Vue.js Approach (Code-Driven Logic) Step 1: Initialize the Project npm create vite@latest vue-modal-app -- --template vue cd vue-modal-app npm install vue-router@4 Step 2: Configure the Routing Paths (src/router.js) import { createRouter, createWebHistory } from 'vue-router' import GalleryView from './views/GalleryView.vue' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: GalleryView, children: [ { path: 'photo/:id', component: () =&gt; import('./components/PhotoModal.vue'), props: true } ] } ] }) Step 3: Link the Router to the Application Update src/main.js: import { createApp } from 'vue' import App from './App.vue' import { router } from './router' createApp(App).use(router).mount('#app') Replace src/App.vue: &lt;template&gt; &lt;router-view /&gt; &lt;/template&gt; Step 4: Create the Gallery List Page (src/views/GalleryView.vue) &lt;template&gt; &lt;div style=\"padding: 20px; font-family: sans-serif;\"&gt; &lt;h1&gt;Vue Photo Gallery&lt;/h1&gt; &lt;div style=\"display: flex; gap: 20px;\"&gt; &lt;div v-for=\"photoId in ['sunset', 'ocean', 'mountains']\" :key=\"photoId\" @click=\"openPhoto(photoId)\" style=\"width: 150px; height: 100px; background: #3b82f6; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 8px;\" &gt; View {{ photoId }} &lt;/div&gt; &lt;/div&gt; &lt;!-- Renders our Child Popup component over the grid --&gt; &lt;router-view v-if=\"showModal\" @close=\"closeModal\" /&gt; &lt;/div&gt; &lt;/template&gt; &lt;script setup&gt; import { ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' const route = useRoute() const router = useRouter() const showModal = ref(false) // Crucial: The Watcher forces the modal open if an ID exists on hard refresh watch( () =&gt; route.params.id, (newId) =&gt; { showModal.value = !!newId }, { immediate: true } ) function openPhoto(id) { router.push(`/photo/${id}`) } function closeModal() { router.push('/') } &lt;/script&gt; Step 5: Create the Popup Modal Component (src/components/PhotoModal.vue) &lt;template&gt; &lt;div style=\"position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;\"&gt; &lt;div style=\"background: white; padding: 30px; border-radius: 12px; text-align: center; width: 300px;\"&gt; &lt;h2&gt;Vue Modal Layout&lt;/h2&gt; &lt;p style=\"font-size: 24px; font-weight: bold; text-transform: uppercase; color: #3b82f6;\"&gt; 🌌 {{ id }} &lt;/p&gt; &lt;button @click=\"$emit('close')\" style=\"margin-top: 20px; padding: 8px 16px; cursor: pointer;\"&gt;Close Overlay&lt;/button&gt; &lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script setup&gt; defineProps({ id: String }) defineEmits(['close']) &lt;/script&gt; Run via npm run dev to view. Project 2: The Next.js Approach (Folder-Driven Architecture) Step 1: Initialize the Project # Configuration flags: TypeScript (Yes), ESLint (No), src/ (No), App Router (Yes) npx create-next-app@latest next-modal-app cd next-modal-app Step 2: Create the File Folders Manually create the exact folders matching the Hierarchy Map above directly inside your editor's file sidebar to prevent system escape errors. Step 3: Set up the Main Gallery Page (app/gallery/page.tsx) import Link from 'next/link'; export default function GalleryPage() { return ( &lt;div style={{ padding: '20px', fontFamily: 'sans-serif' }}&gt; &lt;h1&gt;Next.js Photo Gallery&lt;/h1&gt; &lt;div style={{ display: 'flex', gap: '20px' }}&gt; {['sunset', 'ocean', 'mountains'].map((photoId) =&gt; ( &lt;Link key={photoId} href={`/gallery/photo/${photoId}`} style={{ width: '150px', height: '100px', background: '#ef4444', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', textDecoration: 'none', borderRadius: '8px' }} &gt; View {photoId} &lt;/Link&gt; ))} &lt;/div&gt; &lt;/div&gt; ); } Step 4: Configure the Layout Slots (app/gallery/layout.tsx) export default function GalleryLayout({ children, modal }: { children: React.ReactNode; modal: React.ReactNode; }) { return ( &lt;div&gt; {children} {modal} &lt;/div&gt; ); } Step 5: Create the Slot Fallback (app/gallery/@modal/default.tsx) export default function DefaultModal() { // Fixes the unmounted slot mismatch crash by returning a baseline null return null; } Step 6: Create the Intercepted Popup View (app/gallery/@modal/(.)photo/[id]/page.tsx) 'use client'; import { useRouter, useParams } from 'next/navigation'; export default function InterceptedPhotoPopup() { const router = useRouter(); const params = useParams(); return ( &lt;div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}&gt; &lt;div style={{ background: 'white', padding: '30px', borderRadius: '12px', textAlign: 'center', width: '300px' }}&gt; &lt;h2&gt;Next.js Intercepted Popup&lt;/h2&gt; &lt;p style={{ fontSize: '24px', fontWeight: 'bold', textTransform: 'uppercase', color: '#ef4444' }}&gt; 📸 {params.id} &lt;/p&gt; &lt;button onClick={() =&gt; router.back()} style={{ marginTop: '20px', padding: '8px 16px', cursor: 'pointer' }}&gt; Close Overlay &lt;/button&gt; &lt;/div&gt; &lt;/div&gt; ); } Step 7: Create the Standalone Fallback Page (app/gallery/photo/[id]/page.tsx) import Link from 'next/link'; interface PageProps { params: Promise&lt;{ id: string }&gt;; } export default async function StandalonePhotoPage({ params }: PageProps) { // Modern Next.js rule: params must be unwrapped asynchronously const { id } = await params; return ( &lt;div style={{ padding: '40px', fontFamily: 'sans-serif', textAlign: 'center' }}&gt; &lt;h1&gt;Standalone Full-Screen View&lt;/h1&gt; &lt;div style={{ margin: '40px auto', width: '400px', height: '250px', background: '#ef4444', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '32px', borderRadius: '12px' }}&gt; 🌌 {id.toUpperCase()} &lt;/div&gt; &lt;Link href=\"/gallery\" style={{ color: '#ef4444' }}&gt;← Back to Gallery List&lt;/Link&gt; &lt;/div&gt; ); } Run via npm run dev and navigate to http://localhost:3000/gallery.","contentHash":"sha256:911a0d89ecbe2d93d2247a0b8d402477c5daf7bb9fc477ddb8d6de45b7837168","authorName":"Heba Allah Hashim","authorUrl":"https://zyvop.com/author/heba","authorSameAs":["https://hebamuh68.github.io/hebamuh68/","https://github.com/hebamuh68","https://www.linkedin.com/in/heba333/"],"category":"Tutorial","tags":["webdev","Vue.js"],"audience":"Software engineers and developers building applications with Tutorial","tone":"Instructional, practical, code-first","readingTimeMinutes":4,"wordCount":876,"faqs":null,"primaryTopic":"Tutorial","publishedAt":"2026-08-14T09:07:02.763Z","updatedAt":"2026-08-24T13:50:00.185Z","canonicalUrl":"https://zyvop.com/vue-js-vs-next-js-modal-routing-a-side-by-side-breakdown-pn0kq"}