@ajuarezso/capacitor-liquid-glass 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.es.md ADDED
@@ -0,0 +1,163 @@
1
+ # @ajuarezso/capacitor-liquid-glass
2
+
3
+ > **Chrome nativo Liquid Glass de iOS 26 (TabBar + SearchBar + roadmap para más) en apps Capacitor.**
4
+ > El único plugin Capacitor (a 2026) que expone el `.glassEffect()` auténtico de Apple — no una aproximación con CSS `backdrop-filter`.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@ajuarezso/capacitor-liquid-glass.svg)](https://www.npmjs.com/package/@ajuarezso/capacitor-liquid-glass)
7
+ [![license](https://img.shields.io/npm/l/@ajuarezso/capacitor-liquid-glass.svg)](./LICENSE)
8
+
9
+ > 🇬🇧 English version: [README.md](./README.md)
10
+
11
+ ## Por qué existe
12
+
13
+ iOS 26 introdujo **Liquid Glass** — un material de sistema nuevo que combina blur + refracción dinámica + highlights de borde que se mueven con el contenido subyacente + transiciones de morphing + deformación reactiva al touch + variantes tinted/clear. Apple lo usa en el nuevo TabBar, NavigationBar, Sheets, Menus, el Dynamic Island, la app Music, el App Store, y Control Center.
14
+
15
+ **A 2026, este es el único plugin Capacitor que expone el `UIGlassEffect` real** vía overlays nativos flotando arriba del WebView. Alternativas web como CSS `backdrop-filter`, `mix-blend-mode`, y filtros SVG pueden mimetizar el blur estático pero no reproducen:
16
+
17
+ - Refracción dinámica (la luz se curva según el movimiento)
18
+ - Highlights de borde que se mueven con el contenido subyacente
19
+ - Transiciones morphing entre elementos (ej. el pill del tab deslizándose)
20
+ - Deformación reactiva al touch (el "squish" al presionar)
21
+ - Integración a nivel de sistema con Dynamic Island y Control Center
22
+
23
+ Esto requiere shaders Metal privados que Apple no expone al `WKWebView`. El único camino es un **overlay nativo** anclado arriba del WebView — exactamente lo que hace este plugin.
24
+
25
+ ## Qué hay shipped hoy (v0.3.x)
26
+
27
+ | widget | iOS 26+ | iOS 15-25 | Android | Web |
28
+ |---|---|---|---|---|
29
+ | **TabBar** | ✅ Liquid Glass real | ⚠️ `UITabBar` clásico | ❌ no-op | ❌ no-op |
30
+ | **SearchBar** | ✅ Liquid Glass real | ⚠️ `UISearchBar` clásico en contenedor blurred | ❌ no-op | ❌ no-op |
31
+
32
+ En plataformas no soportadas (Android, Web, iOS < 26) el plugin es un no-op gracioso — tu fallback CSS/HTML sigue funcionando.
33
+
34
+ ## Instalación
35
+
36
+ ```bash
37
+ npm install @ajuarezso/capacitor-liquid-glass
38
+ npx cap sync ios
39
+ ```
40
+
41
+ Target mínimo iOS: `15.0`. **Liquid Glass real requiere iOS 26+**; en versiones anteriores el `UITabBar` / `UISearchBar` nativo todavía renderiza pero sin el material Liquid Glass.
42
+
43
+ ## Quick start
44
+
45
+ ### TabBar
46
+
47
+ ```typescript
48
+ import { LiquidGlass } from '@ajuarezso/capacitor-liquid-glass';
49
+
50
+ await LiquidGlass.showTabBar({
51
+ items: [
52
+ { id: '/home', label: 'Home', sfSymbol: 'house' },
53
+ { id: '/search', label: 'Buscar', sfSymbol: 'magnifyingglass' },
54
+ { id: '/cart', label: 'Carrito', sfSymbol: 'bag', badge: '3' },
55
+ { id: '/profile', label: 'Perfil', sfSymbol: 'person' },
56
+ ],
57
+ selectedIndex: 0,
58
+ tintColor: '#FA7319',
59
+ tabBarStyle: 'liquidGlass',
60
+ });
61
+
62
+ await LiquidGlass.addListener('tabSelected', ({ id, index }) => {
63
+ console.log('Tab tapped:', id, index);
64
+ });
65
+
66
+ await LiquidGlass.updateTabBadge({ id: '/cart', badge: '5' });
67
+ await LiquidGlass.setSelectedTab({ id: '/profile' });
68
+ await LiquidGlass.hideTabBar();
69
+ ```
70
+
71
+ ### SearchBar
72
+
73
+ ```typescript
74
+ await LiquidGlass.showSearchBar({
75
+ placeholder: 'Buscar',
76
+ cancelText: 'Cancelar',
77
+ tintColor: '#FA7319',
78
+ });
79
+
80
+ await LiquidGlass.addListener('searchTextChanged', ({ text }) => {
81
+ console.log('User typed:', text);
82
+ });
83
+ await LiquidGlass.addListener('searchSubmitted', ({ text }) => {
84
+ console.log('User submitted:', text);
85
+ });
86
+ await LiquidGlass.addListener('searchCancelled', () => {
87
+ console.log('User tapped cancel');
88
+ });
89
+
90
+ await LiquidGlass.clearSearchText();
91
+ await LiquidGlass.hideSearchBar();
92
+ ```
93
+
94
+ ## API completa
95
+
96
+ Ver `README.md` (inglés) para la API detallada. Resumen:
97
+
98
+ - **TabBar**: `showTabBar`, `hideTabBar`, `setSelectedTab`, `updateTabBadge`, `getTabBarLayout`
99
+ - **SearchBar**: `showSearchBar`, `hideSearchBar`, `clearSearchText`
100
+ - **Events**: `tabSelected`, `tabBarLayoutChanged`, `searchTextChanged`, `searchSubmitted`, `searchCancelled`
101
+
102
+ ## Comparativa con alternativas
103
+
104
+ | proyecto | plataforma | Liquid Glass real? | activo? |
105
+ |---|---|---|---|
106
+ | **@ajuarezso/capacitor-liquid-glass** | iOS Capacitor | **sí** (`UIGlassEffect` real) | sí (2026) |
107
+ | CSS `backdrop-filter: blur(...)` | todos los webviews | no (solo blur estático) | n/a |
108
+ | `@react-native-community/blur` | React Native | parcial (`UIBlurEffect`, sin `UIGlassEffect`) | sí |
109
+ | `flutter_glassmorphism` | Flutter | no (solo CSS-equivalente) | sí |
110
+
111
+ ## Por qué no solo CSS `backdrop-filter`?
112
+
113
+ Porque es un efecto fundamentalmente distinto:
114
+
115
+ - **CSS `backdrop-filter: blur(20px)`** → solo blur estático de lo que está atrás. Nada más.
116
+ - **iOS 26 Liquid Glass (`UIGlassEffect`)** → blur + refracción dinámica + highlights de borde que se mueven con el contenido + morphing entre elementos (el tab pill que desliza) + deformación reactiva al touch + variantes tinted + integración con Dynamic Island. Usa shaders Metal privados que Apple **no** expone al WKWebView.
117
+
118
+ Si solo necesitás glassmorphism estático para una landing o app no-iOS, usá CSS. Si necesitás el look auténtico iOS 26 en una app Capacitor en iPhone, este plugin es el camino más corto.
119
+
120
+ ## Roadmap
121
+
122
+ - [x] **v0.1**: TabBar con fondo Liquid Glass
123
+ - [x] **v0.2**: SearchBar overlay
124
+ - [x] **v0.3**: Variantes de estilo (`'default'` / `'ultraThin'` / `'transparent'` / `'liquidGlass'`)
125
+ - [ ] NavigationBar (large title, items leading/trailing)
126
+ - [ ] Toolbar (floating toolbar tipo Safari)
127
+ - [ ] Alert (standard y destructive)
128
+ - [ ] Sheet con detents
129
+ - [ ] Menu / Popover
130
+ - [ ] Equivalentes Android Material 3 Expressive
131
+
132
+ PRs welcome.
133
+
134
+ ## Limitaciones
135
+
136
+ 1. **iOS 26 requerido para Liquid Glass real**. En iOS 15-25 el plugin sigue renderizando `UITabBar` / `UISearchBar` nativo pero sin el material Liquid Glass — cae a `UIBlurEffect.systemThinMaterial`.
137
+
138
+ 2. **Android es un no-op**. Equivalentes Material 3 Expressive están en roadmap pero no shipped. Renderizá tu propio fallback.
139
+
140
+ 3. **El overlay nativo flota arriba del WebView**, así que no anima con las transiciones del router web. Usá `hideTabBar()` cuando entrás a rutas fullscreen que no deben mostrar el tab bar.
141
+
142
+ 4. **App Store**: este plugin usa solo APIs públicas de Apple. Sin riesgo de API privada.
143
+
144
+ ## Keywords para discoverability
145
+
146
+ Este plugin resuelve: capacitor liquid glass, ios 26 liquid glass capacitor, capacitor tab bar native, capacitor search bar native, ionic liquid glass, ios 26 UIGlassEffect capacitor, capacitor glassmorphism native, capacitor native chrome, capacitor UITabBar, capacitor UISearchBar, capacitor angular tab bar ios.
147
+
148
+ Proyectos relacionados (alternativa o complementario):
149
+ - CSS `backdrop-filter` (no Liquid Glass real, solo blur)
150
+ - `@capacitor/status-bar` (concern distinto)
151
+ - Librerías de blur para React Native (framework distinto)
152
+ - Plugins community Capacitor para tab bars (HTML/CSS, no Liquid Glass nativo)
153
+
154
+ ## Repositorio
155
+
156
+ - Source: https://github.com/anthonyjuarezsolis/capacitor-liquid-glass
157
+ - Issues: https://github.com/anthonyjuarezsolis/capacitor-liquid-glass/issues
158
+ - npm: https://www.npmjs.com/package/@ajuarezso/capacitor-liquid-glass
159
+ - Construido y verificado en iPhone 17 Pro Max con iOS 26.5
160
+
161
+ ## Licencia
162
+
163
+ MIT © Anthony Juarez Solis — ver [LICENSE](./LICENSE)
package/README.md CHANGED
@@ -1,10 +1,35 @@
1
1
  # @ajuarezso/capacitor-liquid-glass
2
2
 
3
- Native iOS 26 **Liquid Glass** chrome (TabBar, NavigationBar, Alerts, Sheets, Menus) for Capacitor apps. Falls back gracefully on iOS &lt; 26 and Android (no-op), so your app keeps its own CSS chrome on unsupported platforms.
3
+ > **Real iOS 26 Liquid Glass native chrome (TabBar + SearchBar + roadmap for more) for Capacitor apps.**
4
+ > The only Capacitor plugin (as of 2026) that exposes Apple's authentic `.glassEffect()` rendering — not a CSS `backdrop-filter` approximation.
4
5
 
5
- This plugin exists because, as of 2026, no existing Capacitor plugin exposes the real iOS 26 `.glassEffect()` rendered by UIKit — only CSS `backdrop-filter` approximations. This plugin floats a real `UITabBar` (and friends) as a native overlay above your `WKWebView`, so you get the actual Apple-native morphing, refraction, and tint animations of Liquid Glass on iOS 26+.
6
+ [![npm version](https://img.shields.io/npm/v/@ajuarezso/capacitor-liquid-glass.svg)](https://www.npmjs.com/package/@ajuarezso/capacitor-liquid-glass)
7
+ [![license](https://img.shields.io/npm/l/@ajuarezso/capacitor-liquid-glass.svg)](./LICENSE)
6
8
 
7
- > Status: `0.1.0` **TabBar only**. NavigationBar, Toolbar, Sheet, Alert, Menu, Popover are on the roadmap.
9
+ > 🇪🇸 Versión en español: [README.es.md](./README.es.md)
10
+
11
+ ## Why this exists
12
+
13
+ iOS 26 introduced **Liquid Glass** — a new system material that combines blur + dynamic refraction + edge highlights that shift with underlying content + morphing transitions + touch-reactive deformation + tinted/clear variants. Apple uses it for the new TabBar, NavigationBar, Sheets, Menus, the Dynamic Island, the Music app, the App Store, and Control Center.
14
+
15
+ **As of 2026, this is the only Capacitor plugin that exposes the real `UIGlassEffect`** via native overlays floating above the WebView. Web alternatives like CSS `backdrop-filter`, `mix-blend-mode`, and SVG filters can mimic the static blur but cannot reproduce:
16
+
17
+ - Dynamic refraction (light bending based on movement)
18
+ - Edge highlights that shift with underlying content
19
+ - Morphing transitions between elements (e.g., tab pill sliding between items)
20
+ - Touch-reactive deformation (the "squish" when pressed)
21
+ - System-level Dynamic Island and Control Center integration
22
+
23
+ These require private Metal shaders that Apple does not expose to `WKWebView`. The only path is a **native overlay** anchored above the WebView — which is exactly what this plugin does.
24
+
25
+ ## What's shipped today (v0.3.x)
26
+
27
+ | widget | iOS 26+ | iOS 15-25 | Android | Web |
28
+ |---|---|---|---|---|
29
+ | **TabBar** | ✅ Real Liquid Glass | ⚠️ Classic `UITabBar` | ❌ no-op | ❌ no-op |
30
+ | **SearchBar** | ✅ Real Liquid Glass | ⚠️ Classic `UISearchBar` in blurred container | ❌ no-op | ❌ no-op |
31
+
32
+ On unsupported platforms (Android, Web, iOS < 26) the plugin is a graceful no-op — your CSS/HTML fallback continues to work.
8
33
 
9
34
  ## Install
10
35
 
@@ -13,14 +38,15 @@ npm install @ajuarezso/capacitor-liquid-glass
13
38
  npx cap sync ios
14
39
  ```
15
40
 
16
- iOS minimum target: `15.0`. Real Liquid Glass requires **iOS 26+**; on older versions the native `UITabBar` still renders but without the Liquid Glass effect.
41
+ iOS minimum target: `15.0`. **Real Liquid Glass requires iOS 26+**; on older iOS the native `UITabBar` / `UISearchBar` still renders but without the Liquid Glass material.
17
42
 
18
43
  ## Quick start
19
44
 
20
- ```ts
45
+ ### TabBar
46
+
47
+ ```typescript
21
48
  import { LiquidGlass } from '@ajuarezso/capacitor-liquid-glass';
22
49
 
23
- // Show the native tab bar
24
50
  await LiquidGlass.showTabBar({
25
51
  items: [
26
52
  { id: '/home', label: 'Home', sfSymbol: 'house' },
@@ -30,121 +56,255 @@ await LiquidGlass.showTabBar({
30
56
  ],
31
57
  selectedIndex: 0,
32
58
  tintColor: '#FA7319',
59
+ tabBarStyle: 'liquidGlass', // 'default' | 'ultraThin' | 'transparent' | 'liquidGlass'
33
60
  });
34
61
 
35
- // Listen for taps
36
62
  await LiquidGlass.addListener('tabSelected', ({ id, index }) => {
37
63
  console.log('Tab tapped:', id, index);
38
64
  });
39
65
 
40
- // Update badge without rebuilding the tab bar
41
66
  await LiquidGlass.updateTabBadge({ id: '/cart', badge: '5' });
42
-
43
- // Programmatically change the selected tab
44
67
  await LiquidGlass.setSelectedTab({ id: '/profile' });
45
-
46
- // Hide (e.g. when opening a fullscreen modal)
47
68
  await LiquidGlass.hideTabBar();
48
69
  ```
49
70
 
50
- ## API
71
+ ### SearchBar
72
+
73
+ ```typescript
74
+ await LiquidGlass.showSearchBar({
75
+ placeholder: 'Search',
76
+ cancelText: 'Cancel',
77
+ tintColor: '#FA7319',
78
+ });
79
+
80
+ await LiquidGlass.addListener('searchTextChanged', ({ text }) => {
81
+ console.log('User typed:', text);
82
+ });
83
+ await LiquidGlass.addListener('searchSubmitted', ({ text }) => {
84
+ console.log('User submitted:', text);
85
+ });
86
+ await LiquidGlass.addListener('searchCancelled', () => {
87
+ console.log('User tapped cancel');
88
+ });
89
+
90
+ await LiquidGlass.clearSearchText();
91
+ await LiquidGlass.hideSearchBar();
92
+ ```
93
+
94
+ ## API reference
51
95
 
52
- ### `showTabBar(options)`
96
+ ### TabBar
53
97
 
54
- | Option | Type | Required | Description |
55
- | --------------- | ----------------------- | -------- | ----------------------------------------------------------- |
56
- | `items` | `LiquidGlassTabItem[]` | yes | At least one item. |
57
- | `selectedIndex` | `number` | no | Initial selection. Defaults to `0`. |
58
- | `tintColor` | `string` (`#RRGGBB`) | no | Color of the selected pill. Defaults to the system tint. |
98
+ #### `showTabBar(options): Promise<void>`
59
99
 
60
- ### `LiquidGlassTabItem`
100
+ ```typescript
101
+ interface ShowTabBarOptions {
102
+ items: LiquidGlassTabItem[];
103
+ selectedIndex?: number; // default 0
104
+ tintColor?: string; // '#RRGGBB'
105
+ tabBarStyle?: TabBarStyle; // 'default' | 'ultraThin' | 'transparent' | 'liquidGlass'
106
+ containerElement?: string | HTMLElement; // bind to an HTML element (see below)
107
+ }
61
108
 
62
- ```ts
63
109
  interface LiquidGlassTabItem {
64
- /** Stable id emitted in `tabSelected` events. */
65
- id: string;
66
- /** Text under the icon. */
67
- label: string;
68
- /** SF Symbol name (e.g. 'house.fill'). */
69
- sfSymbol: string;
70
- /** Optional badge value (e.g. '3' or '•'). */
71
- badge?: string;
110
+ id: string; // stable id emitted in events
111
+ label: string; // text under the icon
112
+ sfSymbol: string; // SF Symbol name, e.g. 'house.fill'
113
+ badge?: string; // '3' or '•' or undefined
72
114
  }
73
115
  ```
74
116
 
75
- ### `hideTabBar()`
117
+ #### `containerElement` — bind the bar to an HTML element (iOS)
76
118
 
77
- Hides the tab bar without destroying it. Use this when opening fullscreen modals, maps, or any UI that should temporarily own the screen. Call `showTabBar(...)` again to restore it.
119
+ By default the tab bar pins itself to the bottom of the screen. Pass
120
+ `containerElement` (an element `id`, a CSS selector, or the `HTMLElement`) to
121
+ instead **glue the native bar to the bounds of an element in your layout** —
122
+ the same idea as the Capacitor Google Maps placeholder element. ([#1](https://github.com/anthonyjuarezsolis/capacitor-liquid-glass/issues/1))
78
123
 
79
- ### `setSelectedTab({ index?, id? })`
124
+ ```typescript
125
+ await LiquidGlass.showTabBar({
126
+ items: [...],
127
+ containerElement: 'tab-bar-slot', // <div id="tab-bar-slot"> in your DOM
128
+ });
129
+ ```
80
130
 
81
- Updates the selected tab programmatically. Useful when the user navigates via a deep link, a button inside a page, or any source other than the tab bar itself.
131
+ ```html
132
+ <!-- A placeholder that reserves where the native bar should sit. -->
133
+ <div id="tab-bar-slot" style="
134
+ position: fixed; left: 16px; right: 16px; bottom: 24px;
135
+ height: calc(64px + env(safe-area-inset-bottom));">
136
+ </div>
137
+ ```
82
138
 
83
- ### `updateTabBadge({ id, badge })`
139
+ How it works:
84
140
 
85
- Updates a single tab's badge **without** reconfiguring the whole bar (preserves selection). Pass an empty string or omit `badge` to clear.
141
+ - The plugin measures the element's `getBoundingClientRect()` and positions the
142
+ native bar (an on-top overlay) to match — CSS px map 1:1 to UIKit points in a
143
+ WKWebView, so no scaling is applied.
144
+ - It re-syncs automatically on `ResizeObserver` + scroll + rotation + keyboard
145
+ (`visualViewport`), all coalesced through `requestAnimationFrame` so a 120 Hz
146
+ scroll fires at most one reposition per frame.
147
+ - The element is a **layout placeholder only** — keep it empty/transparent; the
148
+ native bar renders on top of it. Resolution and measurement happen in JS; the
149
+ `HTMLElement` never crosses the native bridge.
86
150
 
87
- ### `getTabBarLayout()`
151
+ > ⚠️ **Size the element to include `env(safe-area-inset-bottom)` when it sits at
152
+ > the bottom edge.** The native bar fills exactly the element's rect — if the
153
+ > element is shorter than the bar's natural height (~49 pt + safe area on
154
+ > notched iPhones) the labels/icons get clipped. If the measured rect is `0` in
155
+ > either dimension the plugin falls back to bottom-pinned.
88
156
 
89
- Returns the current `{ height, bottomSafeArea }` in points so you can reserve content padding.
157
+ > ℹ️ Pure **position** changes of a `position: fixed` element that fire no
158
+ > `scroll`/`resize`/`ResizeObserver` event won't auto re-sync. Call
159
+ > `showTabBar(...)` again (cheap — it re-measures) after such a move, or use the
160
+ > low-level `setTabBarBounds(...)` yourself.
90
161
 
91
- ### Events
162
+ #### `setTabBarBounds({ bounds }): Promise<void>`
92
163
 
93
- | Event | Payload |
94
- | ---------------------- | ------------------------------------------- |
95
- | `tabSelected` | `{ index: number, id: string }` |
96
- | `tabBarLayoutChanged` | `{ height: number, bottomSafeArea: number }` |
164
+ Low-level escape hatch (iOS): reposition the bar to an explicit rect
165
+ (`{ x, y, width, height }` in CSS px, viewport-relative). The binding layer
166
+ drives this for you when you pass `containerElement`; call it directly only if
167
+ you measure the element yourself. No-op on web/Android and when the bar is not
168
+ shown.
97
169
 
98
- ## Angular example
170
+ #### `hideTabBar(): Promise<void>`
99
171
 
100
- ```ts
101
- import { bindLiquidGlassNav } from './liquid-glass-nav';
172
+ Hides without destroying configuration. Use for fullscreen modals / maps.
102
173
 
103
- export class CustomerLayout {
104
- private readonly nav = bindLiquidGlassNav({
105
- items: [
106
- { id: '/home', label: 'Home', sfSymbol: 'house' },
107
- { id: '/orders', label: 'Orders', sfSymbol: 'list.bullet.clipboard' },
108
- { id: '/profile', label: 'Profile', sfSymbol: 'person' },
109
- ],
110
- isFullscreen: this.isFullscreen, // signal<boolean>
111
- });
174
+ #### `setSelectedTab({ index?, id? }): Promise<void>`
112
175
 
113
- readonly useNativeTabBar = this.nav.useNativeTabBar;
176
+ Programmatic selection (deep links, internal navigation).
177
+
178
+ #### `updateTabBadge({ id, badge }): Promise<void>`
179
+
180
+ Updates a single badge without reconfiguring the whole bar (preserves selection).
181
+
182
+ #### `getTabBarLayout(): Promise<{ height, bottomSafeArea }>`
183
+
184
+ Returns layout in points to reserve content padding.
185
+
186
+ ### SearchBar
187
+
188
+ #### `showSearchBar(options?): Promise<void>`
189
+
190
+ ```typescript
191
+ interface ShowSearchBarOptions {
192
+ placeholder?: string;
193
+ initialText?: string;
194
+ cancelText?: string;
195
+ tintColor?: string;
196
+ hideCancelButton?: boolean;
114
197
  }
115
198
  ```
116
199
 
117
- See the `example/` folder for a full wiring including router sync and HTML fallback for unsupported platforms.
200
+ #### `hideSearchBar(): Promise<void>`
201
+ #### `clearSearchText(): Promise<void>`
118
202
 
119
- ## Platform behavior
203
+ ### Events
204
+
205
+ | event | payload |
206
+ |---|---|
207
+ | `tabSelected` | `{ index: number, id: string }` |
208
+ | `tabBarLayoutChanged` | `{ height: number, bottomSafeArea: number }` |
209
+ | `searchTextChanged` | `{ text: string }` |
210
+ | `searchSubmitted` | `{ text: string }` |
211
+ | `searchCancelled` | `{}` |
212
+
213
+ ## Angular example
120
214
 
121
- | Widget | iOS 26+ | iOS 15–25 | Android | Web |
122
- | ------------ | -------------------- | ------------------- | ------- | ---------- |
123
- | TabBar | Liquid Glass real | ⚠️ Classic UITabBar | ❌ no-op | ❌ no-op |
215
+ ```typescript
216
+ import { Component, inject, signal } from '@angular/core';
217
+ import { LiquidGlass } from '@ajuarezso/capacitor-liquid-glass';
218
+ import { Capacitor } from '@capacitor/core';
219
+ import { Router } from '@angular/router';
220
+
221
+ @Component({ selector: 'app-shell', template: '<router-outlet />' })
222
+ export class AppShell {
223
+ private router = inject(Router);
224
+
225
+ async ngOnInit() {
226
+ if (!Capacitor.isNativePlatform()) return;
227
+
228
+ await LiquidGlass.showTabBar({
229
+ items: [
230
+ { id: '/home', label: 'Home', sfSymbol: 'house' },
231
+ { id: '/cart', label: 'Cart', sfSymbol: 'bag' },
232
+ ],
233
+ tabBarStyle: 'liquidGlass',
234
+ });
235
+
236
+ LiquidGlass.addListener('tabSelected', ({ id }) => {
237
+ this.router.navigate([id]);
238
+ });
239
+ }
240
+ }
241
+ ```
124
242
 
125
- When the plugin is a no-op (Android, Web, iOS &lt; 26), render your own HTML / CSS fallback. The plugin exposes `Capacitor.getPlatform()` and your Angular / React code can conditionally swap to a `backdrop-filter` bar.
243
+ For unsupported platforms (Android, Web), render your own CSS / Tailwind tab bar gated by `Capacitor.getPlatform()`.
244
+
245
+ ## Comparison with alternatives
246
+
247
+ | project | platform | real Liquid Glass? | adoption | active? |
248
+ |---|---|---|---|---|
249
+ | **@ajuarezso/capacitor-liquid-glass** | iOS Capacitor | **yes** (real `UIGlassEffect`) | Anthony Juarez Solis | yes (2026) |
250
+ | CSS `backdrop-filter: blur(...)` | all webviews | no (just static blur) | universal | n/a |
251
+ | `@react-native-community/blur` | React Native | partial (UIBlurEffect, no `UIGlassEffect`) | RN community | yes |
252
+ | `flutter_glassmorphism` | Flutter | no (just CSS-equivalent) | community | yes |
253
+ | Tauri WKWebView native overlays | Tauri | not yet published | n/a | n/a |
126
254
 
127
255
  ## Why not just CSS `backdrop-filter`?
128
256
 
129
- Because it's not the same thing.
257
+ Because it's a fundamentally different effect:
130
258
 
131
- - **CSS `backdrop-filter`** gives you blur. That's it.
132
- - **Liquid Glass** gives you blur + *dynamic refraction*, *edge highlights that shift with content*, *morphing between elements*, *touch-reactive deformation*, *tinted / clear variants*, and system-level Dynamic Island integration. These require private Metal shaders that Apple does not expose to `WKWebView`.
259
+ - **CSS `backdrop-filter: blur(20px)`** just static blur of what's behind. That's it.
260
+ - **iOS 26 Liquid Glass (`UIGlassEffect`)** blur + dynamic refraction + edge highlights that shift with content motion + morphing between elements (the tab pill that slides) + touch-reactive deformation + tint variants + system Dynamic Island integration. These use private Metal shaders Apple does **not** expose to WKWebView.
133
261
 
134
- This plugin is the shortest path to Apple-authentic Liquid Glass from a Capacitor app.
262
+ If you only need static glassmorphism for a marketing site or non-iOS app, just use CSS. If you need the authentic Apple iOS 26 look on a Capacitor app running on iPhone, this plugin is the shortest path.
135
263
 
136
264
  ## Roadmap
137
265
 
138
- - [x] `TabBar`
139
- - [ ] `NavigationBar` (large title, leading/trailing items)
140
- - [ ] `Toolbar` (floating toolbar like Safari)
141
- - [ ] `Alert` (standard and destructive)
142
- - [ ] `Sheet` (with detents)
143
- - [ ] `Menu` / `Popover`
266
+ - [x] **v0.1**: TabBar with Liquid Glass background
267
+ - [x] **v0.2**: SearchBar overlay
268
+ - [x] **v0.3**: Style variants (`'default'` / `'ultraThin'` / `'transparent'` / `'liquidGlass'`)
269
+ - [x] **v0.4**: Bind the TabBar to an HTML element (`containerElement`) ([#1](https://github.com/anthonyjuarezsolis/capacitor-liquid-glass/issues/1))
270
+ - [ ] NavigationBar (large title, leading/trailing items)
271
+ - [ ] Toolbar (floating toolbar like Safari)
272
+ - [ ] Alert (standard and destructive)
273
+ - [ ] Sheet with detents
274
+ - [ ] Menu / Popover
144
275
  - [ ] Android Material 3 Expressive equivalents
145
276
 
146
277
  PRs welcome.
147
278
 
279
+ ## Limitations
280
+
281
+ 1. **iOS 26 required for real Liquid Glass**. On iOS 15-25 the plugin still renders native `UITabBar` / `UISearchBar` but without the Liquid Glass material — falls back to `UIBlurEffect.systemThinMaterial`.
282
+
283
+ 2. **Android is a no-op**. Material 3 Expressive equivalents are on the roadmap but not yet shipped. Render your own fallback.
284
+
285
+ 3. **The native overlay floats above the WebView**, so it does not animate with router transitions of your web router. Use `hideTabBar()` when entering fullscreen routes that shouldn't show the tab bar.
286
+
287
+ 4. **App Store**: this plugin uses public Apple APIs only. No private API risk.
288
+
289
+ 5. **`containerElement` binding** re-syncs on resize/scroll/rotation/keyboard, but **not** on a pure position move of a `position: fixed` element that emits no DOM event. Re-call `showTabBar(...)` (or `setTabBarBounds(...)`) after such a move. Also size the bound element to include the bottom safe area (see the `containerElement` docs above) or the bar gets clipped.
290
+
291
+ ## Keywords for discoverability
292
+
293
+ This plugin solves: capacitor liquid glass, ios 26 liquid glass capacitor, capacitor tab bar native, capacitor search bar native, ionic liquid glass, ios 26 UIGlassEffect capacitor, capacitor glassmorphism native, capacitor native chrome, capacitor UITabBar, capacitor UISearchBar, capacitor angular tab bar ios, react native vs capacitor liquid glass.
294
+
295
+ Related projects this is an alternative to:
296
+ - CSS `backdrop-filter` (not real Liquid Glass, just blur)
297
+ - `@capacitor/status-bar` (different concern — status bar only)
298
+ - React Native blur libraries (different framework)
299
+ - Capacitor community plugins for tab bars (use HTML/CSS, not native Liquid Glass)
300
+
301
+ ## Repository
302
+
303
+ - Source: https://github.com/anthonyjuarezsolis/capacitor-liquid-glass
304
+ - Issues: https://github.com/anthonyjuarezsolis/capacitor-liquid-glass/issues
305
+ - npm: https://www.npmjs.com/package/@ajuarezso/capacitor-liquid-glass
306
+ - Built and verified on iPhone 17 Pro Max running iOS 26.5
307
+
148
308
  ## License
149
309
 
150
- MIT © Anthony Juarez Solis
310
+ MIT © Anthony Juarez Solis — see [LICENSE](./LICENSE)
@@ -29,6 +29,47 @@ export interface ShowTabBarOptions {
29
29
  tintColor?: string;
30
30
  /** Visual style of the tab bar background. Defaults to `'default'`. */
31
31
  tabBarStyle?: TabBarStyle;
32
+ /**
33
+ * Opt-in: bind the native tab bar to an HTML element's bounds instead of
34
+ * pinning it to the bottom of the screen. Pass an element `id`, a CSS
35
+ * selector, or the `HTMLElement` itself. The plugin measures the element and
36
+ * keeps the native bar glued to it across resize / rotation / keyboard /
37
+ * scroll.
38
+ *
39
+ * When omitted (default), the bar stays pinned to the bottom of the host view
40
+ * — identical to previous behaviour, no regression.
41
+ *
42
+ * Notes:
43
+ * - iOS only. Ignored on web/Android.
44
+ * - The element acts purely as a layout placeholder; size it (width/height
45
+ * incl. safe-area) the way you want the native bar to look. The native bar
46
+ * renders on top of the WebView at the element's rect.
47
+ * - The `HTMLElement`/selector is resolved and stripped in the JS layer; it
48
+ * never crosses the native bridge.
49
+ */
50
+ containerElement?: string | HTMLElement;
51
+ /**
52
+ * Low-level escape hatch: explicit bounds (CSS px, viewport-relative) for the
53
+ * native bar. Normally you pass `containerElement` and the plugin computes
54
+ * this for you. The JS binding layer populates this field before the call
55
+ * reaches native; set it directly only if you manage measurement yourself.
56
+ */
57
+ bounds?: TabBarBounds;
58
+ }
59
+ /**
60
+ * Rect (CSS px, viewport-relative — i.e. straight from
61
+ * `Element.getBoundingClientRect()`) used to position the native tab bar when
62
+ * bound to an HTML element. In a Capacitor WKWebView, CSS px map 1:1 to UIKit
63
+ * points, so no devicePixelRatio scaling is applied.
64
+ */
65
+ export interface TabBarBounds {
66
+ x: number;
67
+ y: number;
68
+ width: number;
69
+ height: number;
70
+ }
71
+ export interface SetTabBarBoundsOptions {
72
+ bounds: TabBarBounds;
32
73
  }
33
74
  export interface SetSelectedTabOptions {
34
75
  /** Either pass numeric index or the item id. */
@@ -89,6 +130,13 @@ export interface LiquidGlassPlugin {
89
130
  updateTabBadge(options: UpdateTabBadgeOptions): Promise<void>;
90
131
  /** Current layout of the tab bar (height + safe area). */
91
132
  getTabBarLayout(): Promise<TabBarLayoutEvent>;
133
+ /**
134
+ * Low-level: reposition the native tab bar to an explicit rect (CSS px,
135
+ * viewport-relative). Driven automatically by the JS binding layer when
136
+ * `showTabBar` was called with `containerElement`; call it directly only if
137
+ * you manage element measurement yourself. No-op if the bar is not shown.
138
+ */
139
+ setTabBarBounds(options: SetTabBarBoundsOptions): Promise<void>;
92
140
  /** Emitted every time the user taps a tab. */
93
141
  addListener(eventName: 'tabSelected', listenerFunc: (event: TabSelectedEvent) => void): Promise<PluginListenerHandle>;
94
142
  /** Emitted when the tab bar's height or safe-area changes (rotation, etc.). */
@@ -1,4 +1,9 @@
1
1
  import type { LiquidGlassPlugin } from './definitions';
2
+ /**
3
+ * Public plugin façade. Everything delegates straight to the native bridge
4
+ * except `showTabBar`/`hideTabBar`, which route through {@link TabBarBinder} so
5
+ * the optional `containerElement` binding works without any consumer wiring.
6
+ */
2
7
  declare const LiquidGlass: LiquidGlassPlugin;
3
8
  export * from './definitions';
4
9
  export { LiquidGlass };
package/dist/esm/index.js CHANGED
@@ -1,6 +1,35 @@
1
- import { registerPlugin } from '@capacitor/core';
2
- const LiquidGlass = registerPlugin('LiquidGlass', {
1
+ import { Capacitor, registerPlugin } from '@capacitor/core';
2
+ import { TabBarBinder } from './tab-bar-binder';
3
+ const native = registerPlugin('LiquidGlass', {
3
4
  web: () => import('./web').then((m) => new m.LiquidGlassWeb()),
4
5
  });
6
+ /**
7
+ * Drives the HTML-element binding lifecycle (measure + observe) on top of the
8
+ * native bridge. When `showTabBar` is called without `containerElement` it is a
9
+ * transparent pass-through, so existing callers behave exactly as before.
10
+ */
11
+ const binder = new TabBarBinder(native);
12
+ /**
13
+ * Public plugin façade. Everything delegates straight to the native bridge
14
+ * except `showTabBar`/`hideTabBar`, which route through {@link TabBarBinder} so
15
+ * the optional `containerElement` binding works without any consumer wiring.
16
+ */
17
+ const LiquidGlass = {
18
+ showTabBar: (options) => binder.showTabBar(options),
19
+ hideTabBar: () => binder.hideTabBar(),
20
+ // iOS-only: the native method only exists on iOS. On Android the bridge would
21
+ // throw "not implemented"; resolve quietly instead (the binding layer already
22
+ // gates on getPlatform() === 'ios', this guards direct low-level callers).
23
+ setTabBarBounds: (options) => Capacitor.getPlatform() === 'ios' ? native.setTabBarBounds(options) : Promise.resolve(),
24
+ setSelectedTab: (options) => native.setSelectedTab(options),
25
+ updateTabBadge: (options) => native.updateTabBadge(options),
26
+ getTabBarLayout: () => native.getTabBarLayout(),
27
+ showSearchBar: (options) => native.showSearchBar(options),
28
+ hideSearchBar: () => native.hideSearchBar(),
29
+ clearSearchText: () => native.clearSearchText(),
30
+ // Preserve the overloaded signature for consumers (the bind keeps `this`).
31
+ addListener: native.addListener.bind(native),
32
+ removeAllListeners: () => native.removeAllListeners(),
33
+ };
5
34
  export * from './definitions';
6
35
  export { LiquidGlass };