@ajuarezso/capacitor-liquid-glass 0.3.7 → 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 +163 -0
- package/README.md +232 -72
- package/dist/esm/definitions.d.ts +48 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +31 -2
- package/dist/esm/tab-bar-binder.d.ts +56 -0
- package/dist/esm/tab-bar-binder.js +186 -0
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +3 -0
- package/dist/plugin.cjs.js +217 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +217 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassPlugin.swift +35 -3
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassTabBarOverlay.swift +153 -31
- package/package.json +1 -1
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
|
+
[](https://www.npmjs.com/package/@ajuarezso/capacitor-liquid-glass)
|
|
7
|
+
[](./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
|
-
|
|
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
|
-
|
|
6
|
+
[](https://www.npmjs.com/package/@ajuarezso/capacitor-liquid-glass)
|
|
7
|
+
[](./LICENSE)
|
|
6
8
|
|
|
7
|
-
>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
96
|
+
### TabBar
|
|
53
97
|
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
117
|
+
#### `containerElement` — bind the bar to an HTML element (iOS)
|
|
76
118
|
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
+
How it works:
|
|
84
140
|
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
+
#### `setTabBarBounds({ bounds }): Promise<void>`
|
|
92
163
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
170
|
+
#### `hideTabBar(): Promise<void>`
|
|
99
171
|
|
|
100
|
-
|
|
101
|
-
import { bindLiquidGlassNav } from './liquid-glass-nav';
|
|
172
|
+
Hides without destroying configuration. Use for fullscreen modals / maps.
|
|
102
173
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
#### `hideSearchBar(): Promise<void>`
|
|
201
|
+
#### `clearSearchText(): Promise<void>`
|
|
118
202
|
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
257
|
+
Because it's a fundamentally different effect:
|
|
130
258
|
|
|
131
|
-
- **CSS `backdrop-filter`**
|
|
132
|
-
- **Liquid Glass**
|
|
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
|
-
|
|
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]
|
|
139
|
-
- [
|
|
140
|
-
- [
|
|
141
|
-
- [
|
|
142
|
-
- [ ]
|
|
143
|
-
- [ ]
|
|
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.). */
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|