Mobile UI with Ionic Vue
Give a Capacitor app stacked pages, native transitions and platform styles with Ionic Vue 9, and refresh data with Ionic's page lifecycle hooks, because pages stay mounted in the stack.
What you'll learn
When you finish, you can do these.- 1Set up Ionic Vue so pages slide in and stack
- 2Load fresh data every time a page comes into view
- 3Wrap each page in IonPage so transitions and hooks work
- 4Give a page opened from a deep link a back button
Picture the notes app this track builds, a small Vue app for writing notes, with a list of notes and a page where you rename one. Packed into a phone app, it still behaves like a website: tapping a note swaps the whole screen at once, and there is no swipe back. People expect a stack of pages: the note slides in from the right, the list waits underneath, and Back slides the note away again.
Ionic Vue, the @ionic/vue package, gives you Vue components that work this way, styled like iOS on an iPhone and like Android on an Android phone. The Code tab holds the notes app built with them: a list page, a note page and the files that set Ionic up. By the end you can build it yourself. You need one idea from Mobile apps with Capacitor: Capacitor wraps your built Vue app, unchanged, in a real Android or iOS app.
Set up Ionic Vue
Ionic goes into a Vue project in four places: its packages, the file that starts the app, the router and the top component. Install @ionic/vue and @ionic/vue-router, Ionic's wrapper around Vue Router, the library that shows the right page for each address.
In src/main.ts, the file that starts the app, import Ionic's CSS and pass IonicVue, the plugin from @ionic/vue that sets Ionic up, to the app's .use(). core.css is required: without it, colours do not show and some components render wrongly. The normalize, structure and typography files are recommended, and the full file in the Code tab adds a few optional ones.
import '@ionic/vue/css/core.css'
import '@ionic/vue/css/normalize.css'
import '@ionic/vue/css/structure.css'
import '@ionic/vue/css/typography.css'
const app = createApp(App).use(IonicVue).use(router)
router.isReady().then(() => {
app.mount('#app')
})
router.isReady() waits until the router knows the first page, so mounting after it keeps that page from sliding in. The router itself, in src/router/index.ts, is built with @ionic/vue-router, which takes the same list of addresses and pages as Vue Router.
In src/App.vue, the top component, IonApp wraps everything, and IonRouterOutlet takes the place of RouterView, the component where Vue Router shows the current page.
<IonApp>
<IonRouterOutlet />
</IonApp>
The outlet shows the page for the current address, as RouterView does, with one difference: it keeps the pages you open.
Pages live in a stack
Keeping pages in a stack changes when a page should load its data. The notes app has two pages in src/views/ in the Code tab: HomePage, the Home page, lists the notes, and NotePage shows one note and lets you rename it.
Tap a note, and the outlet pushes NotePage on top. Home stays mounted underneath: its elements stay in the page, hidden, with their scroll position. Back pops the note page off the stack and destroys it, and Home is shown again as it was, without being mounted a second time.
- Home
- pushNote slides in, Home stays mounted
- backNote destroyed
- Home shownonIonViewWillEnter, not onMounted
So a list loaded in onMounted, Vue's hook that runs a function when a component is mounted, loads once. Rename a note, tap Back, and onMounted does not run again: Home keeps the old title.
Ionic adds four hooks that fire on every visit: onIonViewWillEnter, onIonViewDidEnter, onIonViewWillLeave and onIonViewDidLeave. Load data in onIonViewWillEnter, which runs right before the page slides into view. Home fills its notes list there with listNotes(), a function in src/data/notes.ts that stands in for a server.
<script setup lang="ts">
const notes = ref<Note[]>([])
onIonViewWillEnter(async () => {
notes.value = await listNotes()
})
</script>
Home now loads the notes each time it slides into view, so a renamed note shows its new title after Back.
Ionic's hooks fire only in a page the router shows, and only when IonPage is the outer element of its template. Every such page needs this wrapper: the stack, the transitions and the hooks all look for it, and it sizes IonContent, the scrolling area under the header.
The note page also needs a way back. Its header holds an IonBackButton inside IonButtons, the toolbar's group of buttons, which slot="start" places at the start of the toolbar.
<IonButtons slot="start">
<IonBackButton default-href="/home" />
</IonButtons>
IonBackButton appears only when a page sits below this one in the stack. A deep link opens a page straight from its address: open /notes/2 from a notification or a reload, and the stack holds one page, so the button would be missing and the person stuck. default-href makes it show anyway and go to /home, Home's address.
Native feel, and when to skip it
Beyond the stack, Ionic makes the app look and move like the phone it runs on. It picks a mode for each phone: iOS styling on iPhones, and Material Design, Google's style, on Android.
In iOS mode a swipe from the left edge goes back. Android's back gesture pops a page too, once @capacitor/app, Capacitor's plugin for app events, is installed.
Ionic's CSS also keeps IonHeader, the bar at the top of each page, clear of the status bar, the strip with the phone's clock.
Two Vue tools do not belong here: <Transition>, which animates an element in and out, and <KeepAlive>, which keeps a hidden component alive. The Ionic docs say not to use them, or RouterView, in an Ionic Vue app, because the outlet already animates pages and keeps them.
Ionic is a choice, not a requirement of Capacitor. An internal tool with a clean responsive layout is fine without it. Reach for Ionic when the app has to feel at home next to native apps, with stacked pages, tabs and sheets. Quasar, a Vue framework with its own mobile components, is the main alternative.
What goes wrong
Two mistakes leave an Ionic app running but wrong.
The first loads Home's list in onMounted. You open the note "Call Ben", rename it to "Call Ben back", tap Back, and Home still says "Call Ben". No error appears.
onMounted(async () => {
notes.value = await listNotes()
})
Home stayed mounted the whole time, so the list loaded once and never again. Load on every visit instead, and Home shows "Call Ben back".
onIonViewWillEnter(async () => {
notes.value = await listNotes()
})
The second mistake is a page without IonPage. It still renders, but transitions and Ionic's hooks stop working reliably, and the console shows a warning: [Ionic Warning]: The view you are trying to render for path /notes/2 does not have the required <ion-page> component. Transitions and lifecycle methods may not work as expected.
<template>
<IonHeader>…</IonHeader>
<IonContent class="ion-padding">…</IonContent>
</template>
Put IonPage around the header and the content, and the warning goes away.
<template>
<IonPage>
<IonHeader>…</IonHeader>
<IonContent class="ion-padding">…</IonContent>
</IonPage>
</template>
What to remember
- Register
IonicVueinsrc/main.tsand importcore.cssat least, plus the recommended base styles. IonRouterOutletreplacesRouterViewand keeps a stack: pages below the top one stay mounted.- Going back does not run
onMountedagain. Load data inonIonViewWillEnter, which fires on every visit. - Every page the router shows needs
IonPageas its root, or transitions and Ionic's hooks stop working. - Give
IonBackButtonadefault-href, so a page opened by a deep link still has a way back.
In your project
Do these in a real project. Each one says how to confirm it worked.- Copy the Code tab's files into a Vue project with Ionic installed, run it, and set DevTools to an iPhone. Tap a note, then drag from the left edge: the note follows, and Home shows underneath.
- Change Home to load in
onMounted. Rename a note and go back: the old title stays. Switch back toonIonViewWillEnter, repeat, and the new title appears. - Type
/notes/2in the address bar and press Enter. The note opens with a back button. Removedefault-href, reload, and the button is gone; put it back.