Quasar platform overrides vs runtime checks: an image picker example
Choose between Quasar build-time platform files and runtime Platform checks for multi-target Vue 3 apps.
30 Oct 2026, 14:51 UTC

A file upload that crashes the server
Your Quasar application lets users attach a file. On the web, a standard <input type=\"file\"> works fine. On iOS through Capacitor, you want to open the camera instead. You write one component and guard the native call with Platform.is.ios. It runs in development, but after you add SSR mode, the Node server throws because the Capacitor plugin references window during module import or component setup.
Quasar CLI (Vite-based) can target SPA, SSR, PWA, Capacitor, and Electron from one Vue 3 codebase. The practical question is not whether Quasar supports multiple platforms—it does—but where to split platform-specific code: in separate files resolved at build time, or in runtime checks inside a shared component.
Use filename suffixes such as .ios.vue or .web.vue when the template or imports differ by platform. Use Platform.is only for UI branching that stays safe on the server.
How Quasar resolves platform-specific components
Quasar's Vite build resolves components under src/components/ by checking for platform-specific variants before the base file. A file named ImagePicker.ios.vue is used when building for Capacitor iOS; ImagePicker.vue is used for SPA, SSR, PWA, and Electron unless a more specific variant exists. The resolver strips the iOS-only variant out of non-iOS bundles, so the server never sees @capacitor/camera.
This is a build-time decision, not a runtime if. The wrong variant is never shipped to the client, and the server entry never imports it.
Worked example: image picker across web and Capacitor iOS
First, enable Capacitor in the project root:
cd my-quasar-app
quasar mode add capacitor
Run this in the project root. No elevated permissions are required. The command adds capacitor.config.json and native platform folders; review the git diff before committing because it changes repository state. To roll back, remove capacitor.config.json and the generated ios and android directories.
Create the base web component:
<template>
<q-btn label=\"Choose image\" @click=\"pick\" />
</template>
<script setup>
import { ref } from 'vue'
const image = ref(null)
function pick() {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.onchange = (event) => { image.value = event.target.files[0] }
input.click()
}
</script>
Create the iOS variant:
<template>
<q-btn label=\"Choose image\" @click=\"pick\" />
</template>
<script setup>
import { ref } from 'vue'
import { Camera } from '@capacitor/camera'
const image = ref(null)
async function pick() {
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: 'uri'
})
image.value = photo.webPath
}
</script>
Use the component without platform logic:
<template>
<ImagePicker />
</template>
<script setup>
import ImagePicker from 'src/components/ImagePicker.vue'
</script>
Run quasar dev -m spa. The web build resolves ImagePicker.vue, so no Capacitor plugin is imported and the dev server starts normally. Run quasar dev -m capacitor -T ios on a macOS machine with Xcode command line tools installed; Quasar resolves ImagePicker.ios.vue instead. The iOS target requires Xcode and CocoaPods, so this command fails on Linux or Windows without those dependencies.
Trade-off: duplicated templates and SSR safety
The main limitation is duplication. Both variants above repeat the button template. If the markup grows, extract a shared presentational component and import it into both platform files so only the capture logic differs.
The bigger risk is mixing the two approaches. If you keep one file and call Camera.getPhoto inside setup() guarded by Platform.is.ios, SSR still executes that setup during server rendering unless the import is dynamically deferred. A build-time suffix avoids the problem entirely because the iOS file is not part of the SSR bundle.
You also lose a single test suite for both variants. quasar dev -m spa will not exercise the iOS file, so test the native path on a simulator or device, not just in a desktop browser.
Verify before you ship
- Run
quasar dev -m spaand confirm the file input opens. If the dev server crashes on@capacitor/camera, the plugin is imported outside a platform-specific file. - Run
quasar build -m ssrand inspectdist/ssr/. The server bundle should not contain Capacitor imports. - Run
quasar dev -m capacitor -T iosonly on macOS with Xcode installed, and confirm the camera dialog appears instead of a file input.
If the camera variant works on device and the SSR bundle stays clean, the split is correct. Revert by deleting the .ios.vue file and returning to a single guarded component only if you drop Capacitor iOS support entirely.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.