building platform specific UI components with Vue 3
Ionic Vue provides a component library that behaves like a native Cupertino app on iOS and like a native Material app on Android, without maintaining two separate UI layers. Navigation stacks, gestures, and theming are already built in and can be used directly inside Vue 3 components.
Table of Contents
- 1. What sets Ionic Vue apart from plain UI libraries
- 2. Project setup: bootstrapping Ionic Vue with Vite
- 3. Core components: pages, header, content, cards
- 4. Navigation stacks with the Ionic Router and Vue Router
- 5. Using forms and input components correctly
- 6. Adaptive theming: iOS look and Material look from one codebase
- 7. Native gestures and interaction components
- 8. Working together with Capacitor for native functionality
- 9. Ionic Vue versus a custom Tailwind UI
- 10. Summary
- 11. FAQ
1. What sets Ionic Vue apart from plain UI libraries
Many Vue developers know UI libraries such as Vuetify or Naive UI that provide one unified design system for the browser. Ionic Vue takes a different approach: instead of a single, platform independent look and feel, every component automatically adapts to the operating system the app is running on. An Ionic button looks like a native Cupertino element on iOS and like a native Material Design element on Android, even though both are rendered from the same Vue component.
This adaptive rendering is based on web components, which Ionic uses under the hood, wrapped in Vue 3 compatible wrapper components. For a Vue developer this means writing perfectly normal single file components with <ion-button> or <ion-card> instead of <button> or <div>, while Ionic takes care of platform specific rendering, animations, and interaction patterns behind the scenes.
The essential difference from a plain CSS library is that Ionic Vue delivers not just appearance but also behavior: transitions between pages follow iOS style slide animations or Android style fade animations, back gestures behave like in native apps, and keyboard interactions automatically account for safe area insets. This combination of component library, navigation system, and platform specific behavior makes Ionic Vue its own category between plain web UI and native development.
2. Project setup: bootstrapping Ionic Vue with Vite
Getting started with Ionic Vue works through the official Ionic CLI, which generates a preconfigured Vite project with Vue 3, Vue Router, and the Ionic component library. Alternatively, Ionic Vue can be added to an existing Vue 3 project by installing the core packages and registering them globally. For new projects, the CLI variant is recommended because it immediately provides a sensible folder structure with a tabs navigation starter.
Central to this is registering IonicVue as a Vue plugin in main.js, together with importing the Ionic CSS files for base styles, typography, and utilities. Without these CSS imports, all visual foundations are missing, because Ionic does not deliver its base rendering through classes like classic CSS frameworks, but through shadow DOM encapsulated styles inside the web components.
# Scaffold a new Ionic Vue project with a tabs starter layout
npm install -g @ionic/cli
ionic start mironsoft-app tabs --type=vue
cd mironsoft-app
npm run dev
# Adding Ionic Vue to an existing Vue 3 + Vite project instead
npm install @ionic/vue @ionic/vue-router
// main.js — registering Ionic Vue and its base styles
import { createApp } from 'vue'
import { IonicVue } from '@ionic/vue'
import App from './App.vue'
import router from './router'
// Core Ionic CSS: required for typography, structure, utilities
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')
})
3. Core components: pages, header, content, cards
The basic structure of every page in Ionic Vue follows a fixed pattern: ion-page as the outer container, containing ion-header with a toolbar for title and actions, and ion-content as the scrollable area for the actual page content. This structure is not merely a convention, it is technically required, because ion-content internally handles scroll behavior, pull to refresh, and keyboard adjustment, which would otherwise have to be rebuilt manually in a plain div structure.
For content inside the page, Ionic Vue brings components such as ion-card, ion-list, and ion-item, which mirror typical list patterns found in native apps, including swipe to delete actions via ion-item-sliding. These components automatically handle platform specific rendering, so a list appears with the familiar separator lines on iOS and with Material typical spacing on Android, without the developer having to maintain these differences manually.
<!-- views/ProductListView.vue -->
<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-title>Products</ion-title>
<ion-buttons slot="end">
<ion-button @click="openFilter">Filter</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-refresher slot="fixed" @ionRefresh="handleRefresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<ion-list>
<ion-item-sliding v-for="product in products" :key="product.id">
<ion-item @click="openProduct(product)">
<ion-label>
<span class="item-title">{{ product.name }}</span>
<p>{{ product.price }} €</p>
</ion-label>
</ion-item>
<ion-item-options side="end">
<ion-item-option color="danger" @click="removeFavorite(product)">Remove</ion-item-option>
</ion-item-options>
</ion-item-sliding>
</ion-list>
</ion-content>
</ion-page>
</template>
This example already shows several core building blocks of Ionic Vue working together: pull to refresh via ion-refresher, swipe actions via ion-item-sliding, and a toolbar with title and action buttons. All of these interaction patterns match exactly what users expect from native apps, without needing custom gesture detection or animation logic.
4. Navigation stacks with the Ionic Router and Vue Router
Navigation in Ionic Vue differs from classic web navigation, because native apps manage pages as a stack, not as a simple history. IonRouterOutlet replaces the normal router-view of Vue Router and ensures that when navigating to a new page, the previous page stays in the DOM instead of being destroyed. This enables the typical iOS slide transitions and ensures scroll position and form state are preserved when navigating back.
For nested tab navigation, Ionic Vue combines ion-tabs with independent IonRouterOutlet stacks per tab. That means each tab keeps its own navigation history, exactly like in native tab bar apps: switching from the Products tab to Account and back shows the last visited subpage in the Products tab again, not the start page.
// router/index.js — nested tabs with independent navigation stacks
import { createRouter, createWebHistory } from '@ionic/vue-router'
const routes = [
{
path: '/tabs/',
component: () => import('@/views/TabsPage.vue'),
children: [
{
path: 'products',
component: () => import('@/views/ProductListView.vue'),
},
{
path: 'products/:id',
component: () => import('@/views/ProductDetailView.vue'),
},
{
path: 'account',
component: () => import('@/views/AccountView.vue'),
},
],
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
export default router
Programmatic navigation in Ionic Vue still runs through the familiar useRouter composable from Vue Router, extended with the ability to explicitly control the navigation direction. With router.push({ path: '/tabs/products/42' }) navigation moves forward with the matching slide animation, while router.back() automatically triggers the backward animation, with no manual transition configuration at all.
5. Using forms and input components correctly
Forms in Ionic Vue use components such as ion-input, ion-select, and ion-checkbox, which behave like native HTML elements but are internally implemented as web components with their own shadow DOM. For data binding, v-model works identically on all Ionic form components compared to plain HTML elements, because Ionic maps the necessary ionChange and ionInput events internally to Vue's reactivity system.
One special feature concerns native keyboard interaction: ion-input supports native input types such as tel, email, or number, which automatically bring up the matching virtual keyboard on the device. In addition, Ionic Vue brings platform specific picker components with ion-datetime and ion-picker, which behave like a native wheel picker element on iOS and like a Material dialog on Android.
<!-- Native-feeling form with two-way binding via v-model -->
<ion-list>
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input v-model="form.email" type="email" placeholder="name@company.com"></ion-input>
</ion-item>
<ion-item>
<ion-label position="stacked">Category</ion-label>
<ion-select v-model="form.category" interface="action-sheet">
<ion-select-option value="electronics">Electronics</ion-select-option>
<ion-select-option value="clothing">Clothing</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>Enable notifications</ion-label>
<ion-toggle v-model="form.notifications"></ion-toggle>
</ion-item>
</ion-list>
6. Adaptive theming: iOS look and Material look from one codebase
The theming system of Ionic Vue is based on CSS custom properties defined per color role, such as --ion-color-primary or --ion-color-danger, each with matching contrast and shade values. These variables can be overridden globally in a variables.css to reflect the client's corporate design, without touching individual components. Every Ionic component automatically reads from these variables.
For the difference between iOS and Android, Ionic Vue uses the mode attribute, which can be set globally through the Ionic configuration or per component. In ios mode, shadows appear more subtle, fonts follow San Francisco, and transitions are slide based. In md mode (Material Design), stronger elevation shadows, ripple effects on clicks, and fade transitions appear. By default, Ionic detects the platform automatically from the user agent and chooses the matching mode itself.
/* theme/variables.css — corporate color palette applied to all Ionic components */
:root {
--ion-color-primary: #16a34a;
--ion-color-primary-rgb: 22, 163, 74;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-shade: #138a3f;
--ion-color-primary-tint: #2fb85c;
--ion-color-danger: #dc2626;
--ion-color-danger-rgb: 220, 38, 38;
}
/* Force a specific platform mode for a single component, if needed */
ion-button.force-material {
--border-radius: 4px;
}
7. Native gestures and interaction components
Beyond static UI components, Ionic Vue also ships ready made gesture interactions that would otherwise take considerable effort to implement from scratch. ion-infinite-scroll automatically loads more data once the user reaches the end of a list. ion-reorder-group enables drag and drop reordering of lists via touch gesture, including native haptic feedback on supported devices.
For more complex, custom gestures, Ionic additionally provides the createGesture API, based on the same gesture recognition system that Ionic's own internal components use. This makes it possible to implement custom swipe, pan, or pinch interactions that fit seamlessly into existing Ionic navigation without conflicting with the built in back gestures.
// composables/useSwipeToClose.js
import { onMounted, onBeforeUnmount } from 'vue'
import { createGesture } from '@ionic/vue'
export function useSwipeToClose(elementRef, onClose) {
let gesture
onMounted(() => {
gesture = createGesture({
el: elementRef.value,
gestureName: 'swipe-to-close',
direction: 'y',
onEnd: (detail) => {
if (detail.deltaY > 120) onClose()
},
})
gesture.enable()
})
onBeforeUnmount(() => gesture?.destroy())
}
8. Working together with Capacitor for native functionality
Ionic Vue provides UI components and navigation, but no access to native device functionality such as camera or push notifications. For that part, Ionic Vue is typically combined with Capacitor, the native runtime layer from the same Ionic ecosystem. This combination is no coincidence: both projects are developed by the same team and are deliberately aligned so that Ionic UI components and Capacitor plugins work together without an additional integration layer.
In practice this means: a form built with ion-input collects user input, a Capacitor plugin such as @capacitor/camera delivers a captured photo, and an ion-card displays the result, all within the same Vue component and without a break between the UI layer and the native layer. For teams that need both native look and feel and native functional access, the combination of Ionic Vue and Capacitor is the obvious choice within the Vue ecosystem.
9. Ionic Vue versus a custom Tailwind UI
The choice between Ionic Vue and a self built UI layer with Tailwind depends heavily on how much native behavior and platform specific appearance is desired, versus full design freedom and minimal bundle overhead.
| Criterion | Ionic Vue | Custom Tailwind UI |
|---|---|---|
| Platform specific look | Automatic, iOS and Material out of the box | Must be rebuilt manually |
| Navigation stacks | Ready made with IonRouterOutlet | Requires a custom transition system |
| Design freedom | Via custom properties, but limited | Fully customizable |
| Bundle size | Larger due to the web components layer | Minimal, only classes used |
| Development speed | Very high for standard app patterns | Higher for fully custom design |
For business apps with standard patterns such as lists, forms, and tab navigation, Ionic Vue offers the faster path to a production ready result that immediately feels native. However, once a project demands a strongly custom, brand driven design that deliberately deviates from native platform conventions, a custom Tailwind based design system often becomes the better foundation, because it makes no compromises with predefined Ionic structures.
Mironsoft
Vue development, Ionic apps, and native UI concepts
Native look without two separate codebases?
We design and build Ionic Vue apps with native navigation, platform specific theming, and a clean connection to Capacitor for camera, push, and other native functionality.
Ionic setup
Setting up a project structure with tabs, navigation stacks, and theming
Corporate theming
Implementing brand colors consistently via CSS custom properties
Capacitor integration
Connecting native functionality cleanly to Ionic UI components
10. Summary
Ionic Vue solves a different problem than plain CSS frameworks: it delivers not just appearance, but navigation behavior, gestures, and platform specific interaction patterns that automatically adapt to iOS or Android. The fixed structure of ion-page, ion-header, and ion-content handles scroll behavior and safe area handling, while IonRouterOutlet enables real navigation stacks with preserved component state.
Theming via CSS custom properties allows corporate branding without touching individual components, and combining it with Capacitor closes the gap to native device functionality. For business apps with classic patterns such as lists, forms, and tab navigation, Ionic Vue is the fastest path to an app that feels native on both platforms, without maintaining two separate UI implementations.
Ionic Vue basics — the essentials at a glance
Structure
ion-page, ion-header, ion-content form the required base structure of every Ionic page.
Navigation
IonRouterOutlet replaces the Vue Router view for real navigation stacks with preserved state.
Theming
CSS custom properties such as --ion-color-primary control brand colors globally.
Native integration
Capacitor provides camera, push, and further functionality within the same Ionic ecosystem.