@ajuarezso/capacitor-liquid-glass 0.1.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.
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'CapacitorLiquidGlass'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = 'https://github.com/anthonyjuarezsolis/capacitor-liquid-glass'
11
+ s.author = package['author']
12
+ s.source = { :git => 'https://github.com/anthonyjuarezsolis/capacitor-liquid-glass.git', :tag => s.version.to_s }
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m}'
14
+ s.ios.deployment_target = '15.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.9'
17
+ end
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthony Juarez Solis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Package.swift ADDED
@@ -0,0 +1,26 @@
1
+ // swift-tools-version: 5.9
2
+
3
+ import PackageDescription
4
+
5
+ let package = Package(
6
+ name: "AjuarezsoCapacitorLiquidGlass",
7
+ platforms: [.iOS(.v15)],
8
+ products: [
9
+ .library(
10
+ name: "AjuarezsoCapacitorLiquidGlass",
11
+ targets: ["LiquidGlassPlugin"])
12
+ ],
13
+ dependencies: [
14
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
15
+ ],
16
+ targets: [
17
+ .target(
18
+ name: "LiquidGlassPlugin",
19
+ dependencies: [
20
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
21
+ .product(name: "Cordova", package: "capacitor-swift-pm"),
22
+ ],
23
+ path: "ios/Sources/LiquidGlassPlugin"
24
+ )
25
+ ]
26
+ )
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # @ajuarezso/capacitor-liquid-glass
2
+
3
+ Native iOS 26 **Liquid Glass** chrome (TabBar, NavigationBar, Alerts, Sheets, Menus) for Capacitor apps. Falls back gracefully on iOS < 26 and Android (no-op), so your app keeps its own CSS chrome on unsupported platforms.
4
+
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
+
7
+ > Status: `0.1.0` — **TabBar only**. NavigationBar, Toolbar, Sheet, Alert, Menu, Popover are on the roadmap.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @ajuarezso/capacitor-liquid-glass
13
+ npx cap sync ios
14
+ ```
15
+
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.
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import { LiquidGlass } from '@ajuarezso/capacitor-liquid-glass';
22
+
23
+ // Show the native tab bar
24
+ await LiquidGlass.showTabBar({
25
+ items: [
26
+ { id: '/home', label: 'Home', sfSymbol: 'house' },
27
+ { id: '/search', label: 'Search', sfSymbol: 'magnifyingglass' },
28
+ { id: '/cart', label: 'Cart', sfSymbol: 'bag', badge: '3' },
29
+ { id: '/profile', label: 'Profile', sfSymbol: 'person' },
30
+ ],
31
+ selectedIndex: 0,
32
+ tintColor: '#FA7319',
33
+ });
34
+
35
+ // Listen for taps
36
+ await LiquidGlass.addListener('tabSelected', ({ id, index }) => {
37
+ console.log('Tab tapped:', id, index);
38
+ });
39
+
40
+ // Update badge without rebuilding the tab bar
41
+ await LiquidGlass.updateTabBadge({ id: '/cart', badge: '5' });
42
+
43
+ // Programmatically change the selected tab
44
+ await LiquidGlass.setSelectedTab({ id: '/profile' });
45
+
46
+ // Hide (e.g. when opening a fullscreen modal)
47
+ await LiquidGlass.hideTabBar();
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### `showTabBar(options)`
53
+
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. |
59
+
60
+ ### `LiquidGlassTabItem`
61
+
62
+ ```ts
63
+ 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;
72
+ }
73
+ ```
74
+
75
+ ### `hideTabBar()`
76
+
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.
78
+
79
+ ### `setSelectedTab({ index?, id? })`
80
+
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.
82
+
83
+ ### `updateTabBadge({ id, badge })`
84
+
85
+ Updates a single tab's badge **without** reconfiguring the whole bar (preserves selection). Pass an empty string or omit `badge` to clear.
86
+
87
+ ### `getTabBarLayout()`
88
+
89
+ Returns the current `{ height, bottomSafeArea }` in points so you can reserve content padding.
90
+
91
+ ### Events
92
+
93
+ | Event | Payload |
94
+ | ---------------------- | ------------------------------------------- |
95
+ | `tabSelected` | `{ index: number, id: string }` |
96
+ | `tabBarLayoutChanged` | `{ height: number, bottomSafeArea: number }` |
97
+
98
+ ## Angular example
99
+
100
+ ```ts
101
+ import { bindLiquidGlassNav } from './liquid-glass-nav';
102
+
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
+ });
112
+
113
+ readonly useNativeTabBar = this.nav.useNativeTabBar;
114
+ }
115
+ ```
116
+
117
+ See the `example/` folder for a full wiring including router sync and HTML fallback for unsupported platforms.
118
+
119
+ ## Platform behavior
120
+
121
+ | Widget | iOS 26+ | iOS 15–25 | Android | Web |
122
+ | ------------ | -------------------- | ------------------- | ------- | ---------- |
123
+ | TabBar | ✅ Liquid Glass real | ⚠️ Classic UITabBar | ❌ no-op | ❌ no-op |
124
+
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.
126
+
127
+ ## Why not just CSS `backdrop-filter`?
128
+
129
+ Because it's not the same thing.
130
+
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`.
133
+
134
+ This plugin is the shortest path to Apple-authentic Liquid Glass from a Capacitor app.
135
+
136
+ ## Roadmap
137
+
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`
144
+ - [ ] Android Material 3 Expressive equivalents
145
+
146
+ PRs welcome.
147
+
148
+ ## License
149
+
150
+ MIT © Anthony Juarez Solis
@@ -0,0 +1,56 @@
1
+ import type { PluginListenerHandle } from '@capacitor/core';
2
+ export interface LiquidGlassTabItem {
3
+ /** Stable id used in tab selection events. */
4
+ id: string;
5
+ /** Label rendered under the icon. */
6
+ label: string;
7
+ /** SF Symbol name for iOS (e.g. "house.fill"). */
8
+ sfSymbol: string;
9
+ /** Optional badge value (e.g. "3" or "•"). */
10
+ badge?: string;
11
+ }
12
+ export interface ShowTabBarOptions {
13
+ items: LiquidGlassTabItem[];
14
+ /** Index of the initially selected item. Defaults to 0. */
15
+ selectedIndex?: number;
16
+ /** Tint color for selected state, hex "#RRGGBB". Defaults to iOS system tint. */
17
+ tintColor?: string;
18
+ }
19
+ export interface SetSelectedTabOptions {
20
+ /** Either pass numeric index or the item id. */
21
+ index?: number;
22
+ id?: string;
23
+ }
24
+ export interface UpdateTabBadgeOptions {
25
+ /** Tab item id whose badge should change. */
26
+ id: string;
27
+ /** New badge value; pass empty string or omit to clear it. */
28
+ badge?: string;
29
+ }
30
+ export interface TabSelectedEvent {
31
+ index: number;
32
+ id: string;
33
+ }
34
+ export interface TabBarLayoutEvent {
35
+ /** Total height of the tab bar including internal padding (pt). */
36
+ height: number;
37
+ /** Safe-area bottom inset the tab bar is sitting on (pt). */
38
+ bottomSafeArea: number;
39
+ }
40
+ export interface LiquidGlassPlugin {
41
+ /** Creates (or updates) and shows the native Liquid Glass tab bar. */
42
+ showTabBar(options: ShowTabBarOptions): Promise<void>;
43
+ /** Hides the tab bar without destroying configuration. */
44
+ hideTabBar(): Promise<void>;
45
+ /** Updates the currently selected tab. */
46
+ setSelectedTab(options: SetSelectedTabOptions): Promise<void>;
47
+ /** Updates a single tab's badge without reconfiguring the whole bar. */
48
+ updateTabBadge(options: UpdateTabBadgeOptions): Promise<void>;
49
+ /** Current layout of the tab bar (height + safe area). */
50
+ getTabBarLayout(): Promise<TabBarLayoutEvent>;
51
+ /** Emitted every time the user taps a tab. */
52
+ addListener(eventName: 'tabSelected', listenerFunc: (event: TabSelectedEvent) => void): Promise<PluginListenerHandle>;
53
+ /** Emitted when the tab bar's height or safe-area changes (rotation, etc.). */
54
+ addListener(eventName: 'tabBarLayoutChanged', listenerFunc: (event: TabBarLayoutEvent) => void): Promise<PluginListenerHandle>;
55
+ removeAllListeners(): Promise<void>;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { LiquidGlassPlugin } from './definitions';
2
+ declare const LiquidGlass: LiquidGlassPlugin;
3
+ export * from './definitions';
4
+ export { LiquidGlass };
@@ -0,0 +1,6 @@
1
+ import { registerPlugin } from '@capacitor/core';
2
+ const LiquidGlass = registerPlugin('LiquidGlass', {
3
+ web: () => import('./web').then((m) => new m.LiquidGlassWeb()),
4
+ });
5
+ export * from './definitions';
6
+ export { LiquidGlass };
@@ -0,0 +1,15 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ import type { LiquidGlassPlugin, SetSelectedTabOptions, ShowTabBarOptions, TabBarLayoutEvent, UpdateTabBadgeOptions } from './definitions';
3
+ /**
4
+ * Web fallback — real Liquid Glass requires native iOS 26. On the web we
5
+ * resolve no-ops so the app can run in the browser during development; the
6
+ * Angular shell is expected to render its own CSS glassmorphism tab bar when
7
+ * `isNativePlatform()` is false.
8
+ */
9
+ export declare class LiquidGlassWeb extends WebPlugin implements LiquidGlassPlugin {
10
+ showTabBar(_options: ShowTabBarOptions): Promise<void>;
11
+ hideTabBar(): Promise<void>;
12
+ setSelectedTab(_options: SetSelectedTabOptions): Promise<void>;
13
+ updateTabBadge(_options: UpdateTabBadgeOptions): Promise<void>;
14
+ getTabBarLayout(): Promise<TabBarLayoutEvent>;
15
+ }
@@ -0,0 +1,24 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ /**
3
+ * Web fallback — real Liquid Glass requires native iOS 26. On the web we
4
+ * resolve no-ops so the app can run in the browser during development; the
5
+ * Angular shell is expected to render its own CSS glassmorphism tab bar when
6
+ * `isNativePlatform()` is false.
7
+ */
8
+ export class LiquidGlassWeb extends WebPlugin {
9
+ async showTabBar(_options) {
10
+ // no-op on web
11
+ }
12
+ async hideTabBar() {
13
+ // no-op on web
14
+ }
15
+ async setSelectedTab(_options) {
16
+ // no-op on web
17
+ }
18
+ async updateTabBadge(_options) {
19
+ // no-op on web
20
+ }
21
+ async getTabBarLayout() {
22
+ return { height: 0, bottomSafeArea: 0 };
23
+ }
24
+ }
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ var core = require('@capacitor/core');
4
+
5
+ const LiquidGlass = core.registerPlugin('LiquidGlass', {
6
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.LiquidGlassWeb()),
7
+ });
8
+
9
+ /**
10
+ * Web fallback — real Liquid Glass requires native iOS 26. On the web we
11
+ * resolve no-ops so the app can run in the browser during development; the
12
+ * Angular shell is expected to render its own CSS glassmorphism tab bar when
13
+ * `isNativePlatform()` is false.
14
+ */
15
+ class LiquidGlassWeb extends core.WebPlugin {
16
+ async showTabBar(_options) {
17
+ // no-op on web
18
+ }
19
+ async hideTabBar() {
20
+ // no-op on web
21
+ }
22
+ async setSelectedTab(_options) {
23
+ // no-op on web
24
+ }
25
+ async updateTabBadge(_options) {
26
+ // no-op on web
27
+ }
28
+ async getTabBarLayout() {
29
+ return { height: 0, bottomSafeArea: 0 };
30
+ }
31
+ }
32
+
33
+ var web = /*#__PURE__*/Object.freeze({
34
+ __proto__: null,
35
+ LiquidGlassWeb: LiquidGlassWeb
36
+ });
37
+
38
+ exports.LiquidGlass = LiquidGlass;
39
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiquidGlass = registerPlugin('LiquidGlass', {\n web: () => import('./web').then((m) => new m.LiquidGlassWeb()),\n});\nexport * from './definitions';\nexport { LiquidGlass };\n","import { WebPlugin } from '@capacitor/core';\n/**\n * Web fallback — real Liquid Glass requires native iOS 26. On the web we\n * resolve no-ops so the app can run in the browser during development; the\n * Angular shell is expected to render its own CSS glassmorphism tab bar when\n * `isNativePlatform()` is false.\n */\nexport class LiquidGlassWeb extends WebPlugin {\n async showTabBar(_options) {\n // no-op on web\n }\n async hideTabBar() {\n // no-op on web\n }\n async setSelectedTab(_options) {\n // no-op on web\n }\n async updateTabBadge(_options) {\n // no-op on web\n }\n async getTabBarLayout() {\n return { height: 0, bottomSafeArea: 0 };\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,WAAW,GAAGA,mBAAc,CAAC,aAAa,EAAE;AAClD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;AAClE,CAAC;;ACFD;AACA;AACA;AACA;AACA;AACA;AACO,MAAM,cAAc,SAASC,cAAS,CAAC;AAC9C,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B;AACA,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB;AACA,IAAI;AACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;AACnC;AACA,IAAI;AACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;AACnC;AACA,IAAI;AACJ,IAAI,MAAM,eAAe,GAAG;AAC5B,QAAQ,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE;AAC/C,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js ADDED
@@ -0,0 +1,42 @@
1
+ var capacitorLiquidGlass = (function (exports, core) {
2
+ 'use strict';
3
+
4
+ const LiquidGlass = core.registerPlugin('LiquidGlass', {
5
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.LiquidGlassWeb()),
6
+ });
7
+
8
+ /**
9
+ * Web fallback — real Liquid Glass requires native iOS 26. On the web we
10
+ * resolve no-ops so the app can run in the browser during development; the
11
+ * Angular shell is expected to render its own CSS glassmorphism tab bar when
12
+ * `isNativePlatform()` is false.
13
+ */
14
+ class LiquidGlassWeb extends core.WebPlugin {
15
+ async showTabBar(_options) {
16
+ // no-op on web
17
+ }
18
+ async hideTabBar() {
19
+ // no-op on web
20
+ }
21
+ async setSelectedTab(_options) {
22
+ // no-op on web
23
+ }
24
+ async updateTabBadge(_options) {
25
+ // no-op on web
26
+ }
27
+ async getTabBarLayout() {
28
+ return { height: 0, bottomSafeArea: 0 };
29
+ }
30
+ }
31
+
32
+ var web = /*#__PURE__*/Object.freeze({
33
+ __proto__: null,
34
+ LiquidGlassWeb: LiquidGlassWeb
35
+ });
36
+
37
+ exports.LiquidGlass = LiquidGlass;
38
+
39
+ return exports;
40
+
41
+ })({}, capacitorExports);
42
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiquidGlass = registerPlugin('LiquidGlass', {\n web: () => import('./web').then((m) => new m.LiquidGlassWeb()),\n});\nexport * from './definitions';\nexport { LiquidGlass };\n","import { WebPlugin } from '@capacitor/core';\n/**\n * Web fallback — real Liquid Glass requires native iOS 26. On the web we\n * resolve no-ops so the app can run in the browser during development; the\n * Angular shell is expected to render its own CSS glassmorphism tab bar when\n * `isNativePlatform()` is false.\n */\nexport class LiquidGlassWeb extends WebPlugin {\n async showTabBar(_options) {\n // no-op on web\n }\n async hideTabBar() {\n // no-op on web\n }\n async setSelectedTab(_options) {\n // no-op on web\n }\n async updateTabBadge(_options) {\n // no-op on web\n }\n async getTabBarLayout() {\n return { height: 0, bottomSafeArea: 0 };\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,WAAW,GAAGA,mBAAc,CAAC,aAAa,EAAE;IAClD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;IAClE,CAAC;;ICFD;IACA;IACA;IACA;IACA;IACA;IACO,MAAM,cAAc,SAASC,cAAS,CAAC;IAC9C,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B;IACA,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB;IACA,IAAI;IACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC;IACA,IAAI;IACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC;IACA,IAAI;IACJ,IAAI,MAAM,eAAe,GAAG;IAC5B,QAAQ,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE;IAC/C,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -0,0 +1,123 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import UIKit
4
+
5
+ @objc(LiquidGlassPlugin)
6
+ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
7
+ public let identifier = "LiquidGlassPlugin"
8
+ public let jsName = "LiquidGlass"
9
+ public let pluginMethods: [CAPPluginMethod] = [
10
+ CAPPluginMethod(name: "showTabBar", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "hideTabBar", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "setSelectedTab", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "updateTabBadge", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "getTabBarLayout", returnType: CAPPluginReturnPromise),
15
+ ]
16
+
17
+ private var tabBarOverlay: LiquidGlassTabBarOverlay?
18
+
19
+ @objc func showTabBar(_ call: CAPPluginCall) {
20
+ guard let rawItems = call.getArray("items") as? [[String: Any]] else {
21
+ call.reject("items is required")
22
+ return
23
+ }
24
+ let selectedIndex = call.getInt("selectedIndex") ?? 0
25
+ let tintHex = call.getString("tintColor")
26
+
27
+ let items = rawItems.compactMap { LiquidGlassTabItem(dictionary: $0) }
28
+ guard !items.isEmpty else {
29
+ call.reject("items cannot be empty")
30
+ return
31
+ }
32
+
33
+ DispatchQueue.main.async { [weak self] in
34
+ guard let self else { return }
35
+ self.presentTabBar(items: items, selectedIndex: selectedIndex, tintHex: tintHex)
36
+ call.resolve()
37
+ }
38
+ }
39
+
40
+ @objc func hideTabBar(_ call: CAPPluginCall) {
41
+ DispatchQueue.main.async { [weak self] in
42
+ self?.tabBarOverlay?.hide()
43
+ call.resolve()
44
+ }
45
+ }
46
+
47
+ @objc func updateTabBadge(_ call: CAPPluginCall) {
48
+ guard let id = call.getString("id") else {
49
+ call.reject("id is required")
50
+ return
51
+ }
52
+ let badge = call.getString("badge")
53
+
54
+ DispatchQueue.main.async { [weak self] in
55
+ self?.tabBarOverlay?.updateBadge(id: id, badge: badge)
56
+ call.resolve()
57
+ }
58
+ }
59
+
60
+ @objc func setSelectedTab(_ call: CAPPluginCall) {
61
+ let index = call.getInt("index")
62
+ let id = call.getString("id")
63
+
64
+ DispatchQueue.main.async { [weak self] in
65
+ guard let overlay = self?.tabBarOverlay else {
66
+ call.reject("tab bar is not shown")
67
+ return
68
+ }
69
+ if let index {
70
+ overlay.setSelectedIndex(index)
71
+ } else if let id {
72
+ overlay.setSelected(id: id)
73
+ }
74
+ call.resolve()
75
+ }
76
+ }
77
+
78
+ @objc func getTabBarLayout(_ call: CAPPluginCall) {
79
+ DispatchQueue.main.async { [weak self] in
80
+ let layout = self?.tabBarOverlay?.currentLayout() ?? (height: 0.0, bottomSafeArea: 0.0)
81
+ call.resolve([
82
+ "height": layout.height,
83
+ "bottomSafeArea": layout.bottomSafeArea,
84
+ ])
85
+ }
86
+ }
87
+
88
+ private func presentTabBar(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?) {
89
+ guard let window = UIApplication.shared.capacitorWindow else { return }
90
+
91
+ if tabBarOverlay == nil {
92
+ let overlay = LiquidGlassTabBarOverlay()
93
+ overlay.onTabSelected = { [weak self] index, id in
94
+ self?.notifyListeners("tabSelected", data: ["index": index, "id": id])
95
+ }
96
+ overlay.onLayoutChanged = { [weak self] height, bottomSafeArea in
97
+ self?.notifyListeners("tabBarLayoutChanged", data: [
98
+ "height": height,
99
+ "bottomSafeArea": bottomSafeArea,
100
+ ])
101
+ }
102
+ tabBarOverlay = overlay
103
+ }
104
+
105
+ tabBarOverlay?.attach(to: window)
106
+ tabBarOverlay?.configure(items: items, selectedIndex: selectedIndex, tintHex: tintHex)
107
+ tabBarOverlay?.show()
108
+ }
109
+ }
110
+
111
+ // MARK: - UIApplication helper
112
+ private extension UIApplication {
113
+ var capacitorWindow: UIWindow? {
114
+ return connectedScenes
115
+ .compactMap { $0 as? UIWindowScene }
116
+ .flatMap { $0.windows }
117
+ .first { $0.isKeyWindow } ??
118
+ connectedScenes
119
+ .compactMap { $0 as? UIWindowScene }
120
+ .flatMap { $0.windows }
121
+ .first
122
+ }
123
+ }
@@ -0,0 +1,162 @@
1
+ import UIKit
2
+
3
+ struct LiquidGlassTabItem {
4
+ let id: String
5
+ let label: String
6
+ let sfSymbol: String
7
+ let badge: String?
8
+
9
+ init?(dictionary: [String: Any]) {
10
+ guard
11
+ let id = dictionary["id"] as? String,
12
+ let label = dictionary["label"] as? String,
13
+ let sfSymbol = dictionary["sfSymbol"] as? String
14
+ else {
15
+ return nil
16
+ }
17
+ self.id = id
18
+ self.label = label
19
+ self.sfSymbol = sfSymbol
20
+ self.badge = dictionary["badge"] as? String
21
+ }
22
+ }
23
+
24
+ /// A floating UITabBar overlay that adopts iOS 26 Liquid Glass automatically
25
+ /// when the app is built against the iOS 26 SDK. On earlier iOS the bar falls
26
+ /// back to the translucent blurred UITabBar appearance.
27
+ final class LiquidGlassTabBarOverlay: NSObject {
28
+
29
+ // Public callbacks
30
+ var onTabSelected: ((Int, String) -> Void)?
31
+ var onLayoutChanged: ((Double, Double) -> Void)?
32
+
33
+ // Internal state
34
+ private weak var window: UIWindow?
35
+ private var tabBar: UITabBar?
36
+ private var items: [LiquidGlassTabItem] = []
37
+
38
+ func attach(to window: UIWindow) {
39
+ if self.window === window, tabBar != nil { return }
40
+ self.window = window
41
+
42
+ let tabBar = UITabBar()
43
+ tabBar.translatesAutoresizingMaskIntoConstraints = false
44
+ tabBar.delegate = self
45
+
46
+ // Default appearance triggers Liquid Glass automatically on iOS 26.
47
+ let appearance = UITabBarAppearance()
48
+ appearance.configureWithDefaultBackground()
49
+ tabBar.standardAppearance = appearance
50
+ if #available(iOS 15.0, *) {
51
+ tabBar.scrollEdgeAppearance = appearance
52
+ }
53
+
54
+ window.addSubview(tabBar)
55
+
56
+ NSLayoutConstraint.activate([
57
+ tabBar.leadingAnchor.constraint(equalTo: window.leadingAnchor),
58
+ tabBar.trailingAnchor.constraint(equalTo: window.trailingAnchor),
59
+ tabBar.bottomAnchor.constraint(equalTo: window.bottomAnchor),
60
+ ])
61
+
62
+ self.tabBar = tabBar
63
+ emitLayout()
64
+ }
65
+
66
+ func configure(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?) {
67
+ guard let tabBar else { return }
68
+
69
+ // Preserve selection across re-configs (badge changes, etc.)
70
+ let previousSelectedId: String? = tabBar.selectedItem.flatMap { current in
71
+ self.items.indices.contains(current.tag) ? self.items[current.tag].id : nil
72
+ }
73
+
74
+ self.items = items
75
+
76
+ let uiItems: [UITabBarItem] = items.enumerated().map { index, item in
77
+ let tab = UITabBarItem(
78
+ title: item.label,
79
+ image: UIImage(systemName: item.sfSymbol),
80
+ tag: index
81
+ )
82
+ if let badge = item.badge, !badge.isEmpty {
83
+ tab.badgeValue = badge
84
+ }
85
+ return tab
86
+ }
87
+
88
+ tabBar.setItems(uiItems, animated: false)
89
+
90
+ if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
91
+ tabBar.selectedItem = uiItems[restoreIdx]
92
+ } else if selectedIndex >= 0, selectedIndex < uiItems.count {
93
+ tabBar.selectedItem = uiItems[selectedIndex]
94
+ }
95
+
96
+ if let tintHex, let tint = UIColor(hex: tintHex) {
97
+ tabBar.tintColor = tint
98
+ }
99
+ emitLayout()
100
+ }
101
+
102
+ func updateBadge(id: String, badge: String?) {
103
+ guard let tabBar, let tabBarItems = tabBar.items,
104
+ let idx = items.firstIndex(where: { $0.id == id }),
105
+ idx < tabBarItems.count else { return }
106
+ let value = (badge?.isEmpty ?? true) ? nil : badge
107
+ tabBarItems[idx].badgeValue = value
108
+ }
109
+
110
+ func show() {
111
+ tabBar?.isHidden = false
112
+ emitLayout()
113
+ }
114
+
115
+ func hide() {
116
+ tabBar?.isHidden = true
117
+ }
118
+
119
+ func setSelectedIndex(_ index: Int) {
120
+ guard let tabBar, let items = tabBar.items, index >= 0, index < items.count else { return }
121
+ tabBar.selectedItem = items[index]
122
+ }
123
+
124
+ func setSelected(id: String) {
125
+ guard let index = items.firstIndex(where: { $0.id == id }) else { return }
126
+ setSelectedIndex(index)
127
+ }
128
+
129
+ func currentLayout() -> (height: Double, bottomSafeArea: Double) {
130
+ guard let tabBar else { return (0, 0) }
131
+ tabBar.layoutIfNeeded()
132
+ let bottomInset = tabBar.safeAreaInsets.bottom
133
+ return (Double(tabBar.frame.height), Double(bottomInset))
134
+ }
135
+
136
+ private func emitLayout() {
137
+ let layout = currentLayout()
138
+ onLayoutChanged?(layout.height, layout.bottomSafeArea)
139
+ }
140
+ }
141
+
142
+ // MARK: - UITabBarDelegate
143
+ extension LiquidGlassTabBarOverlay: UITabBarDelegate {
144
+ func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
145
+ guard item.tag >= 0, item.tag < items.count else { return }
146
+ let selected = items[item.tag]
147
+ onTabSelected?(item.tag, selected.id)
148
+ }
149
+ }
150
+
151
+ // MARK: - UIColor+hex
152
+ private extension UIColor {
153
+ convenience init?(hex: String) {
154
+ var value = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
155
+ if value.hasPrefix("#") { value.removeFirst() }
156
+ guard value.count == 6, let rgb = UInt64(value, radix: 16) else { return nil }
157
+ let r = CGFloat((rgb & 0xFF0000) >> 16) / 255
158
+ let g = CGFloat((rgb & 0x00FF00) >> 8) / 255
159
+ let b = CGFloat(rgb & 0x0000FF) / 255
160
+ self.init(red: r, green: g, blue: b, alpha: 1.0)
161
+ }
162
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@ajuarezso/capacitor-liquid-glass",
3
+ "version": "0.1.0",
4
+ "description": "iOS 26 Liquid Glass native chrome (TabBar, NavigationBar, Alerts, Sheets) for Capacitor apps. Falls back gracefully on iOS < 26 and Android.",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "dist/",
11
+ "ios/Sources/",
12
+ "Package.swift",
13
+ "CapacitorLiquidGlass.podspec"
14
+ ],
15
+ "author": "Anthony Juarez Solis <anthonyjuarezsolis@icloud.com>",
16
+ "license": "MIT",
17
+ "keywords": [
18
+ "capacitor",
19
+ "plugin",
20
+ "ios",
21
+ "liquid-glass",
22
+ "ios-26",
23
+ "tabbar",
24
+ "navigation",
25
+ "glassmorphism"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/anthonyjuarezsolis/capacitor-liquid-glass.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/anthonyjuarezsolis/capacitor-liquid-glass/issues"
33
+ },
34
+ "homepage": "https://github.com/anthonyjuarezsolis/capacitor-liquid-glass#readme",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "npm run clean && npm run tsc && rollup -c rollup.config.mjs",
40
+ "clean": "rimraf ./dist",
41
+ "tsc": "tsc",
42
+ "watch": "tsc --watch"
43
+ },
44
+ "devDependencies": {
45
+ "@capacitor/core": "^8.0.0",
46
+ "rimraf": "^5.0.0",
47
+ "rollup": "^4.9.0",
48
+ "typescript": "^5.4.0"
49
+ },
50
+ "peerDependencies": {
51
+ "@capacitor/core": "^8.0.0"
52
+ },
53
+ "capacitor": {
54
+ "ios": {
55
+ "src": "ios"
56
+ }
57
+ }
58
+ }