A mobile application built with Svelte 5 and Ionic Framework, demonstrating modern mobile app development patterns including bottom tab navigation and stack-based page navigation.
src/
├── components/
│ └── NavWrapper.svelte # ion-nav wrapper component
├── lib/
│ └── Counter.svelte # Demo counter component
├── pages/
│ ├── Home.svelte # Home tab with product list
│ ├── Search.svelte # Search tab with search bar
│ ├── Favorite.svelte # Favorites tab
│ ├── Settings.svelte # Settings tab
│ └── details/
│ ├── ItemDetail.svelte # Product detail page
│ └── SubDetail.svelte # Nested detail page
├── utils/
│ └── svelteDelegate.ts # Framework delegate for ion-nav
├── App.svelte # Root component with tabs
├── main.ts # App initialization
├── ionic.d.ts # Ionic type declarations
└── vite-env.d.ts # Vite & Svelte type declarations
# Install dependencies
npm install
# Start development server
npm run dev
# Type check
npm run check
# Build for production
npm run build
# Preview production build
npm run preview
Custom framework delegate to make ion-nav work with Svelte components:
// src/utils/svelteDelegate.ts
export const svelteDelegate = {
attachViewToDom: async (parentElement, component, props) => {
const wrapper = document.createElement('div')
wrapper.classList.add('ion-page')
const instance = mount(component, { target: wrapper, props })
parentElement.appendChild(wrapper)
return wrapper
},
removeViewFromDom: async (_, childElement) => {
const instance = instanceMap.get(childElement)
if (instance) unmount(instance)
childElement.remove()
}
}
Wraps each tab content with ion-nav for independent navigation stacks:
<!-- src/components/NavWrapper.svelte -->
<script lang="ts">
import { onMount } from 'svelte'
import { svelteDelegate } from '../utils/svelteDelegate'
let { root, rootParams = {} } = $props()
let navElement = $state(null)
onMount(async () => {
if (navElement && root) {
navElement.delegate = svelteDelegate
await navElement.setRoot(root, rootParams)
}
})
</script>
<ion-nav bind:this={navElement}></ion-nav>
Push new pages onto the navigation stack:
const handleItemClick = async (itemId: number, itemName: string) => {
const nav = document.querySelector('ion-nav') as HTMLIonNavElement
if (nav) {
const module = await import('./details/ItemDetail.svelte')
await nav.push(module.default, { itemId, itemName })
}
}
Each tab maintains its own navigation stack:
Home Tab: Search Tab:
├── Home (root) ├── Search (root)
└── ItemDetail └── ItemDetail
└── SubDetail └── SubDetail
Switching between tabs preserves the navigation state.
All interactive elements include:
Full TypeScript support with custom declarations for:
This project follows tutorials from nextbeat-engineering on Medium:
Feel free to open issues or submit PRs for improvements!
MIT