---
title: Capacitor
description: Use touchcn in a Capacitor Angular, React, or Vue app — safe areas, status bar overlay, platform detection, and keyboard behavior.
sidebar:
  label: Capacitor
  icon: '<svg viewBox="0 0 24 24" width="100%" height="100%" fill="currentColor" aria-hidden="true"><path d="M24 3.7l-5.766 5.766 5.725 5.736-3.713 3.712L5.073 3.742 8.786.03l5.736 5.726L20.284 0 24 3.7zM.029 8.785l3.713-3.713 15.173 15.173-3.713 3.714-5.732-5.726L3.7 24 0 20.285l5.754-5.764L.029 8.785z"/></svg>'
---

touchcn is a web-first component library, so inside a [Capacitor](https://capacitorjs.com) app it runs
exactly as it does in the browser — it renders in the native WebView (WKWebView on iOS, Android System
WebView on Android). The install, Tailwind wiring, and provider setup are unchanged from
[Getting started](/docs/getting-started); this guide covers only what a native shell adds on top:
drawing under the system chrome, syncing the status bar, and platform detection.

## Install

Follow [Getting started](/docs/getting-started) to install the engine packages, wire up Tailwind v4,
register the provider, and add components. Nothing about that changes for Capacitor — the same
`@touchcn/*` packages and the same CLI-copied sources run in the WebView.

## Draw under the system chrome (`viewport-fit=cover`)

touchcn's navbar, tabbar, and bottom-anchored sheets reserve space for the device's safe areas using
the CSS `env(safe-area-inset-*)` variables — the iOS navbar adds `env(safe-area-inset-top)` to its
top padding, the tabbar and sheets add `env(safe-area-inset-bottom)` to their bottom padding, and the
`.tcn-safe-*` helper classes expose the same insets.

Those variables only resolve to non-zero values when the viewport is told to extend into the safe
areas. Add `viewport-fit=cover` to the viewport meta tag in your `index.html`:

```html index.html
<meta
  name="viewport"
  content="viewport-fit=cover, width=device-width, initial-scale=1.0"
/>
```

Without `viewport-fit=cover`, `env(safe-area-inset-*)` resolves to `0` and touchcn's chrome sits flush
against the screen edges — sliding under the notch, Dynamic Island, or home indicator.

## Status bar overlay

For touchcn's safe-area chrome to own the top inset, the WebView should draw **under** the status bar
rather than being pushed below it. Install the official status bar plugin:

```bash
npm install @capacitor/status-bar
```

Enable overlay mode in `capacitor.config.ts` so it applies at launch:

```ts capacitor.config.ts
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.app',
  appName: 'My App',
  webDir: 'dist',
  plugins: {
    StatusBar: {
      overlaysWebView: true,
      style: 'DEFAULT',
    },
  },
};

export default config;
```

With `overlaysWebView: true`, content draws beneath the status bar and touchcn's
`env(safe-area-inset-top)` padding on the navbar keeps it clear.

:::note
`overlaysWebView` and `backgroundColor` are not available on Android 15+, where the system enforces an
edge-to-edge, transparent status bar. On those versions the WebView already draws under the status bar,
which is what touchcn's safe-area handling expects.
:::

### Sync the status bar style with the color scheme

The `@capacitor/status-bar` `Style` enum is named by the **text color**, not the background:
`Style.Dark` renders *light* text (for a dark background), `Style.Light` renders *dark* text (for a
light background), and `Style.Default` follows the device appearance. Drive it from touchcn's resolved
color scheme so the status bar text stays legible when the theme flips:

<CodeGroup>

```ts Angular
import { effect, inject, Injectable } from '@angular/core';
import { StatusBar, Style } from '@capacitor/status-bar';
import { TouchcnAppearance } from '@touchcn/angular';

@Injectable({ providedIn: 'root' })
export class StatusBarSync {
  private readonly appearance = inject(TouchcnAppearance);

  constructor() {
    // `appearance.scheme` is a signal — the effect re-runs whenever it changes.
    effect(() => {
      StatusBar.setStyle({ style: this.appearance.scheme() === 'dark' ? Style.Dark : Style.Light });
    });
  }
}
```

```tsx React
import { useEffect } from 'react';
import { StatusBar, Style } from '@capacitor/status-bar';
import { useTouchcnAppearance } from '@touchcn/react';

export function useStatusBarSync() {
  const { scheme } = useTouchcnAppearance();

  useEffect(() => {
    StatusBar.setStyle({ style: scheme === 'dark' ? Style.Dark : Style.Light });
  }, [scheme]);
}
```

```ts Vue
import { watchEffect } from 'vue';
import { StatusBar, Style } from '@capacitor/status-bar';
import { useTouchcnAppearance } from '@touchcn/vue';

export function useStatusBarSync() {
  // `scheme` is a ref — the effect re-runs whenever it changes.
  const { scheme } = useTouchcnAppearance();

  watchEffect(() => {
    StatusBar.setStyle({ style: scheme.value === 'dark' ? Style.Dark : Style.Light });
  });
}
```

</CodeGroup>

:::note
`StatusBar.setStyle()` is a no-op on the web platform, so guard the call behind
`Capacitor.isNativePlatform()` if the same code path also runs in a browser.
:::

## Platform detection

touchcn's `detectPlatform()` reads `navigator.userAgent`. In a Capacitor WebView that user-agent
carries the native platform: iOS reports `iPhone` / `iPad`, Android reports `Android`. As a result the
theme resolves correctly with **no extra configuration** — iOS renders the `ios` (liquid-glass) theme,
Android renders the `md` (Material 3) theme, and the provider toggles the `.ios` / `.md` classes on
`<html>` automatically. iPadOS is handled too (it is detected via `navigator.platform` and
`maxTouchPoints`).

To force a theme regardless of the device — for a screenshot build, a design preview, or a deliberate
single-platform look — override it explicitly:

<CodeGroup>

```ts Angular
import { inject } from '@angular/core';
import { TouchcnAppearance } from '@touchcn/angular';

const appearance = inject(TouchcnAppearance);
appearance.setTheme('ios'); // force 'ios' or 'md'; pass null to fall back to detection
```

```tsx React
import { TouchcnProvider } from '@touchcn/react';

// Force the theme for the whole subtree; omit `theme` to auto-detect.
<TouchcnProvider theme="ios">
  <App />
</TouchcnProvider>
```

```vue Vue
<!-- App.vue — force the theme for the whole app; omit `theme` to auto-detect. -->
<script setup lang="ts">
import { provideTouchcn } from '@touchcn/vue';

provideTouchcn({ theme: 'ios' }); // force 'ios' or 'md'
</script>
```

</CodeGroup>

## Keyboard

The [`@capacitor/keyboard`](https://capacitorjs.com/docs/apis/keyboard) plugin controls how the WebView
reacts when the software keyboard appears. Its `resize` config accepts `native` (the default — the
whole WebView resizes, so `vh`-based heights shrink), `body`, `ionic`, or `none`.

With the default `native` mode, the viewport shrinks when the keyboard opens: a bottom-anchored
`tcn-tabbar` and any `position: fixed` touchcn overlays move up with the new viewport bottom, and the
top `tcn-navbar` stays pinned. That is usually the behavior you want for a focused input above the
keyboard. If you switch to `body`, relative units are left untouched (the `env(safe-area-inset-*)`
insets still apply), so verify that overlays and the tabbar still land where you expect. On Android,
`resizeOnFullScreen: true` is the documented workaround for the WebView not resizing while the status
bar overlays the app.
