react-native-magic-tab-bar 1.0.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/LICENSE +21 -0
- package/README.md +121 -0
- package/lib/module/MagicTabBar.js +130 -0
- package/lib/module/MagicTabBar.js.map +1 -0
- package/lib/module/MagicTabItem.js +96 -0
- package/lib/module/MagicTabItem.js.map +1 -0
- package/lib/module/MagicTabs.js +60 -0
- package/lib/module/MagicTabs.js.map +1 -0
- package/lib/module/defaultTabs.js +52 -0
- package/lib/module/defaultTabs.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/theme.js +16 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/MagicTabBar.d.ts +38 -0
- package/lib/typescript/MagicTabBar.d.ts.map +1 -0
- package/lib/typescript/MagicTabItem.d.ts +28 -0
- package/lib/typescript/MagicTabItem.d.ts.map +1 -0
- package/lib/typescript/MagicTabs.d.ts +49 -0
- package/lib/typescript/MagicTabs.d.ts.map +1 -0
- package/lib/typescript/defaultTabs.d.ts +10 -0
- package/lib/typescript/defaultTabs.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +10 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/theme.d.ts +4 -0
- package/lib/typescript/theme.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +48 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/package.json +91 -0
- package/src/MagicTabBar.tsx +181 -0
- package/src/MagicTabItem.tsx +123 -0
- package/src/MagicTabs.tsx +87 -0
- package/src/defaultTabs.tsx +55 -0
- package/src/index.tsx +15 -0
- package/src/theme.ts +15 -0
- package/src/types.ts +51 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { MagicTabBarTheme, MagicTabBarVariant, MagicTabConfig } from './types';
|
|
3
|
+
export interface MagicTabsProps {
|
|
4
|
+
/**
|
|
5
|
+
* The tabs to render, in order. Each entry maps a route to an icon + label.
|
|
6
|
+
* When omitted, a default set is used — Home, Explore, Notifications, Inbox
|
|
7
|
+
* and Profile — with matching Ionicons (see {@link defaultTabs}).
|
|
8
|
+
*/
|
|
9
|
+
tabs?: MagicTabConfig[];
|
|
10
|
+
/** Override any part of the default theme. */
|
|
11
|
+
theme?: Partial<MagicTabBarTheme>;
|
|
12
|
+
/** Position the bar floating over content (default) or docked in-flow. */
|
|
13
|
+
variant?: MagicTabBarVariant;
|
|
14
|
+
/**
|
|
15
|
+
* Make the bar background see-through. Off by default. When `true`, control
|
|
16
|
+
* the strength with `transparency`.
|
|
17
|
+
*/
|
|
18
|
+
isTransparent?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Opacity of the bar background while `isTransparent` is true, from 0 to 1
|
|
21
|
+
* (e.g. `0.4` = 40% visible). Clamped to a minimum so the bar never fully
|
|
22
|
+
* disappears. Defaults to 0.6.
|
|
23
|
+
*/
|
|
24
|
+
transparency?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Render the bar as native iOS Liquid Glass (via `expo-glass-effect`).
|
|
27
|
+
* Requires iOS 26+; on any other platform it falls back to the translucent
|
|
28
|
+
* `barColor`. No drop shadow is drawn in glass/transparent mode.
|
|
29
|
+
*/
|
|
30
|
+
glass?: boolean;
|
|
31
|
+
/** Render a custom background (e.g. a blur/glass view) behind the bar. */
|
|
32
|
+
renderBackground?: () => ReactNode;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A drop-in custom tab bar for Expo Router.
|
|
36
|
+
*
|
|
37
|
+
* Use it in an `app/_layout.tsx` and pass your routes, icons and labels as props:
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <MagicTabs
|
|
41
|
+
* tabs={[
|
|
42
|
+
* { name: 'index', href: '/', label: 'Home', icon: ({ color }) => <Home color={color} /> },
|
|
43
|
+
* { name: 'search', href: '/search', label: 'Search', icon: ({ color }) => <Search color={color} /> },
|
|
44
|
+
* ]}
|
|
45
|
+
* />
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function MagicTabs({ tabs, theme: themeOverride, variant, isTransparent, transparency, glass, renderBackground, }: MagicTabsProps): import("react").JSX.Element;
|
|
49
|
+
//# sourceMappingURL=MagicTabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MagicTabs.d.ts","sourceRoot":"","sources":["../../src/MagicTabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEpF,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC;IACxB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,MAAM,SAAS,CAAC;CACpC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,SAAS,CAAC,EACxB,IAAkB,EAClB,KAAK,EAAE,aAAa,EACpB,OAAO,EACP,aAAa,EACb,YAAY,EACZ,KAAK,EACL,gBAAgB,GACjB,EAAE,cAAc,+BAwBhB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MagicTabConfig } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* The tabs used when `<MagicTabs />` is rendered without a `tabs` prop:
|
|
4
|
+
* Home, Explore, Notifications, Inbox and Profile, each with a matching Ionicon.
|
|
5
|
+
*
|
|
6
|
+
* Assumes the app has routes named `index` (`/`), `explore`, `notifications`,
|
|
7
|
+
* `inbox` and `profile`. Pass your own `tabs` to `<MagicTabs />` to override.
|
|
8
|
+
*/
|
|
9
|
+
export declare const defaultTabs: MagicTabConfig[];
|
|
10
|
+
//# sourceMappingURL=defaultTabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaultTabs.d.ts","sourceRoot":"","sources":["../../src/defaultTabs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,SAAS,CAAC;AAcjE;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,cAAc,EA+BvC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { MagicTabs } from './MagicTabs';
|
|
2
|
+
export { MagicTabBar } from './MagicTabBar';
|
|
3
|
+
export { MagicTabItem } from './MagicTabItem';
|
|
4
|
+
export { defaultTabs } from './defaultTabs';
|
|
5
|
+
export { defaultTheme } from './theme';
|
|
6
|
+
export type { MagicTabsProps } from './MagicTabs';
|
|
7
|
+
export type { MagicTabBarProps } from './MagicTabBar';
|
|
8
|
+
export type { MagicTabItemProps } from './MagicTabItem';
|
|
9
|
+
export type { MagicTabConfig, MagicTabIconProps, MagicTabBarTheme, MagicTabBarVariant, } from './types';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,yCAAyC;AACzC,eAAO,MAAM,YAAY,EAAE,gBAW1B,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Href } from 'expo-router';
|
|
3
|
+
/** Props passed to each tab's `icon` render function. */
|
|
4
|
+
export interface MagicTabIconProps {
|
|
5
|
+
/** Whether this tab is the active one. */
|
|
6
|
+
focused: boolean;
|
|
7
|
+
/** Resolved color for the icon (active or inactive color from the theme). */
|
|
8
|
+
color: string;
|
|
9
|
+
/** Icon size in points (from the theme). */
|
|
10
|
+
size: number;
|
|
11
|
+
}
|
|
12
|
+
/** Configuration for a single tab. */
|
|
13
|
+
export interface MagicTabConfig {
|
|
14
|
+
/** Route name — must match the file in the `app/` directory (e.g. `"index"`, `"search"`). */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Destination href, e.g. `"/"` or `"/search"`. */
|
|
17
|
+
href: Href;
|
|
18
|
+
/** Optional text label rendered next to the icon while the tab is active. */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Renders the tab's icon. */
|
|
21
|
+
icon: (props: MagicTabIconProps) => ReactNode;
|
|
22
|
+
}
|
|
23
|
+
/** Visual configuration for the tab bar. */
|
|
24
|
+
export interface MagicTabBarTheme {
|
|
25
|
+
/** Background color of the bar (ignored when `renderBackground` is provided). */
|
|
26
|
+
barColor: string;
|
|
27
|
+
/** Background color of the pill behind the active tab. */
|
|
28
|
+
activePillColor: string;
|
|
29
|
+
/** Icon/label color for the active tab. */
|
|
30
|
+
activeColor: string;
|
|
31
|
+
/** Icon color for inactive tabs. */
|
|
32
|
+
inactiveColor: string;
|
|
33
|
+
/** Icon size in points. */
|
|
34
|
+
iconSize: number;
|
|
35
|
+
/** Font size of the active tab's label, in points. */
|
|
36
|
+
fontSize: number;
|
|
37
|
+
/** Height of the bar. */
|
|
38
|
+
height: number;
|
|
39
|
+
/** Corner radius of the bar and the active pill. */
|
|
40
|
+
radius: number;
|
|
41
|
+
/** Horizontal margin between the bar and the screen edges. */
|
|
42
|
+
horizontalMargin: number;
|
|
43
|
+
/** Extra space below the bar, added on top of the safe-area inset. */
|
|
44
|
+
bottomInset: number;
|
|
45
|
+
}
|
|
46
|
+
/** How the bar is positioned relative to screen content. */
|
|
47
|
+
export type MagicTabBarVariant = 'floating' | 'docked';
|
|
48
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,IAAI,EAAE,IAAI,CAAC;IACX,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,IAAI,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,SAAS,CAAC;CAC/C;AAED,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,eAAe,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,gBAAgB,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,4DAA4D;AAC5D,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,QAAQ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-magic-tab-bar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Customizable floating tab bar for Expo Router (SDK 56+). Bring your own icons and labels.",
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"example"
|
|
7
|
+
],
|
|
8
|
+
"main": "./lib/module/index.js",
|
|
9
|
+
"module": "./lib/module/index.js",
|
|
10
|
+
"types": "./lib/typescript/index.d.ts",
|
|
11
|
+
"source": "./src/index.tsx",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./lib/typescript/index.d.ts",
|
|
15
|
+
"default": "./lib/module/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"lib",
|
|
22
|
+
"!**/__tests__",
|
|
23
|
+
"!**/*.test.*",
|
|
24
|
+
"!**/*.spec.*"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "bob build",
|
|
28
|
+
"clean": "del-cli lib",
|
|
29
|
+
"prepare": "bob build",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"example": "npm --prefix example run start"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"react-native",
|
|
35
|
+
"expo",
|
|
36
|
+
"expo-router",
|
|
37
|
+
"tab-bar",
|
|
38
|
+
"tabbar",
|
|
39
|
+
"tabs",
|
|
40
|
+
"bottom-tabs",
|
|
41
|
+
"navigation",
|
|
42
|
+
"ios",
|
|
43
|
+
"android"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/Bhavinpethani04/react-native-magic-tab-bar.git"
|
|
48
|
+
},
|
|
49
|
+
"author": "Bhavin Pethani <pethanibhavin004@gmail.com>",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@expo/vector-icons": "^15.0.2"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"expo": "*",
|
|
56
|
+
"expo-glass-effect": ">=56.0.0",
|
|
57
|
+
"expo-router": ">=56.0.0",
|
|
58
|
+
"react": "*",
|
|
59
|
+
"react-native": "*",
|
|
60
|
+
"react-native-reanimated": ">=3.0.0",
|
|
61
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
62
|
+
"react-native-worklets": "*"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"react-native-worklets": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"expo-glass-effect": {
|
|
69
|
+
"optional": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/react": "~19.2.2",
|
|
74
|
+
"del-cli": "^6.0.0",
|
|
75
|
+
"react-native-builder-bob": "^0.40.0",
|
|
76
|
+
"typescript": "~6.0.3"
|
|
77
|
+
},
|
|
78
|
+
"react-native-builder-bob": {
|
|
79
|
+
"source": "src",
|
|
80
|
+
"output": "lib",
|
|
81
|
+
"targets": [
|
|
82
|
+
[
|
|
83
|
+
"module",
|
|
84
|
+
{
|
|
85
|
+
"esm": true
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"typescript"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { forwardRef, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
StyleSheet,
|
|
4
|
+
View,
|
|
5
|
+
type View as RNView,
|
|
6
|
+
type ViewProps,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
9
|
+
import type { MagicTabBarTheme, MagicTabBarVariant } from "./types";
|
|
10
|
+
|
|
11
|
+
declare const require: (moduleName: string) => unknown;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `expo-glass-effect` is an optional peer dependency. We load it through a
|
|
15
|
+
* guarded `require` so the library still installs and runs for consumers who
|
|
16
|
+
* don't need (or install) it — when it's absent, `glass` mode silently falls
|
|
17
|
+
* back to the translucent `barColor`. The try/catch lets Metro treat it as an
|
|
18
|
+
* optional dependency instead of failing the bundle.
|
|
19
|
+
*/
|
|
20
|
+
const glassEffect = (() => {
|
|
21
|
+
try {
|
|
22
|
+
return require("expo-glass-effect") as typeof import("expo-glass-effect");
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
/** Lowest bar opacity we allow, so a transparent bar never becomes invisible. */
|
|
29
|
+
export const MIN_BAR_OPACITY = 0.1;
|
|
30
|
+
|
|
31
|
+
export interface MagicTabBarProps extends ViewProps {
|
|
32
|
+
/** Resolved theme. Provided automatically by `MagicTabs`. */
|
|
33
|
+
theme: MagicTabBarTheme;
|
|
34
|
+
/** Position the bar floating over content (default) or docked in-flow. */
|
|
35
|
+
variant?: MagicTabBarVariant;
|
|
36
|
+
/**
|
|
37
|
+
* Make the bar background see-through. Off by default — the bar is fully
|
|
38
|
+
* opaque. Set the strength of the effect with `transparency`.
|
|
39
|
+
*/
|
|
40
|
+
isTransparent?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Opacity of the bar background while `isTransparent` is true, from 0 to 1
|
|
43
|
+
* (e.g. `0.4` = 40% visible). Clamped to a minimum of {@link MIN_BAR_OPACITY}
|
|
44
|
+
* so the bar never disappears. Defaults to 0.6 when omitted.
|
|
45
|
+
*/
|
|
46
|
+
transparency?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Render the bar as native iOS Liquid Glass (via `expo-glass-effect`).
|
|
49
|
+
* Requires iOS 26+; on any other platform/version it automatically falls
|
|
50
|
+
* back to the translucent `barColor` (honoring `transparency`).
|
|
51
|
+
*/
|
|
52
|
+
glass?: boolean;
|
|
53
|
+
/** Render a custom background (e.g. a blur/glass view) behind the bar. */
|
|
54
|
+
renderBackground?: () => ReactNode;
|
|
55
|
+
/** The tab items. Provided automatically by `MagicTabs`. */
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The visual container of the tab bar. Designed to be used as the `asChild`
|
|
61
|
+
* target of an Expo Router `<TabList>`.
|
|
62
|
+
*/
|
|
63
|
+
export const MagicTabBar = forwardRef<RNView, MagicTabBarProps>(
|
|
64
|
+
function MagicTabBar(
|
|
65
|
+
{
|
|
66
|
+
theme,
|
|
67
|
+
variant = "floating",
|
|
68
|
+
isTransparent = false,
|
|
69
|
+
transparency = 0.6,
|
|
70
|
+
glass = false,
|
|
71
|
+
renderBackground,
|
|
72
|
+
children,
|
|
73
|
+
style,
|
|
74
|
+
...rest
|
|
75
|
+
},
|
|
76
|
+
ref,
|
|
77
|
+
) {
|
|
78
|
+
const insets = useSafeAreaInsets();
|
|
79
|
+
const floating = variant !== "docked";
|
|
80
|
+
// Native Liquid Glass needs the optional `expo-glass-effect` dep and iOS
|
|
81
|
+
// 26+; everywhere else we fall back to the translucent color background.
|
|
82
|
+
const useGlass = glass && !!glassEffect?.isLiquidGlassAvailable();
|
|
83
|
+
// Only a transparent bar fades; otherwise it stays fully opaque. The level
|
|
84
|
+
// is clamped so it never drops below MIN_BAR_OPACITY or above 1.
|
|
85
|
+
const barOpacity = isTransparent
|
|
86
|
+
? Math.min(Math.max(transparency, MIN_BAR_OPACITY), 1)
|
|
87
|
+
: 1;
|
|
88
|
+
// A see-through bar shouldn't cast a hard drop shadow — it reads as an odd
|
|
89
|
+
// halo around the translucent fill. Keep the shadow only for a solid bar.
|
|
90
|
+
const seeThrough = useGlass || isTransparent;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<View
|
|
94
|
+
ref={ref}
|
|
95
|
+
pointerEvents="box-none"
|
|
96
|
+
style={[
|
|
97
|
+
floating ? styles.floatingWrapper : styles.dockedWrapper,
|
|
98
|
+
{
|
|
99
|
+
paddingHorizontal: theme.horizontalMargin,
|
|
100
|
+
paddingBottom: (floating ? insets.bottom : 0) + theme.bottomInset,
|
|
101
|
+
},
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
<View
|
|
105
|
+
style={[
|
|
106
|
+
styles.bar,
|
|
107
|
+
!seeThrough && styles.barShadow,
|
|
108
|
+
{ height: theme.height, borderRadius: theme.radius },
|
|
109
|
+
]}
|
|
110
|
+
>
|
|
111
|
+
{renderBackground ? (
|
|
112
|
+
<View
|
|
113
|
+
pointerEvents="none"
|
|
114
|
+
style={[
|
|
115
|
+
StyleSheet.absoluteFill,
|
|
116
|
+
{ borderRadius: theme.radius, overflow: "hidden" },
|
|
117
|
+
]}
|
|
118
|
+
>
|
|
119
|
+
{renderBackground()}
|
|
120
|
+
</View>
|
|
121
|
+
) : useGlass && glassEffect ? (
|
|
122
|
+
// Native iOS Liquid Glass. We tint it with the bar color so themes
|
|
123
|
+
// still carry through, and clip it to the bar's rounded corners.
|
|
124
|
+
<glassEffect.GlassView
|
|
125
|
+
pointerEvents="none"
|
|
126
|
+
glassEffectStyle="regular"
|
|
127
|
+
tintColor={theme.barColor}
|
|
128
|
+
style={[StyleSheet.absoluteFill, { borderRadius: theme.radius }]}
|
|
129
|
+
/>
|
|
130
|
+
) : (
|
|
131
|
+
// Background color lives in its own layer so `transparency` fades
|
|
132
|
+
// only the bar's fill, never the icons or labels on top of it.
|
|
133
|
+
<View
|
|
134
|
+
pointerEvents="none"
|
|
135
|
+
style={[
|
|
136
|
+
StyleSheet.absoluteFill,
|
|
137
|
+
{
|
|
138
|
+
backgroundColor: theme.barColor,
|
|
139
|
+
borderRadius: theme.radius,
|
|
140
|
+
opacity: barOpacity,
|
|
141
|
+
},
|
|
142
|
+
]}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
<View style={[styles.row, style]} {...rest}>
|
|
146
|
+
{children}
|
|
147
|
+
</View>
|
|
148
|
+
</View>
|
|
149
|
+
</View>
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const styles = StyleSheet.create({
|
|
155
|
+
floatingWrapper: {
|
|
156
|
+
position: "absolute",
|
|
157
|
+
left: 0,
|
|
158
|
+
right: 0,
|
|
159
|
+
bottom: 0,
|
|
160
|
+
},
|
|
161
|
+
dockedWrapper: {
|
|
162
|
+
width: "100%",
|
|
163
|
+
},
|
|
164
|
+
bar: {
|
|
165
|
+
flexDirection: "row",
|
|
166
|
+
},
|
|
167
|
+
barShadow: {
|
|
168
|
+
shadowColor: "#000",
|
|
169
|
+
shadowOpacity: 0.25,
|
|
170
|
+
shadowRadius: 16,
|
|
171
|
+
shadowOffset: { width: 0, height: 8 },
|
|
172
|
+
elevation: 12,
|
|
173
|
+
},
|
|
174
|
+
row: {
|
|
175
|
+
flex: 1,
|
|
176
|
+
flexDirection: "row",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
justifyContent: "space-around",
|
|
179
|
+
paddingHorizontal: 6,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { forwardRef, useEffect, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
type GestureResponderEvent,
|
|
6
|
+
type View as RNView,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import Animated, {
|
|
9
|
+
FadeIn,
|
|
10
|
+
LinearTransition,
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
useSharedValue,
|
|
13
|
+
withSpring,
|
|
14
|
+
} from 'react-native-reanimated';
|
|
15
|
+
import type { MagicTabBarTheme, MagicTabIconProps } from './types';
|
|
16
|
+
|
|
17
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
18
|
+
|
|
19
|
+
const SPRING = { mass: 0.6, damping: 18, stiffness: 180 } as const;
|
|
20
|
+
const transition = LinearTransition.springify()
|
|
21
|
+
.mass(SPRING.mass)
|
|
22
|
+
.damping(SPRING.damping)
|
|
23
|
+
.stiffness(SPRING.stiffness);
|
|
24
|
+
|
|
25
|
+
export interface MagicTabItemProps {
|
|
26
|
+
/** Renders the icon. Provided automatically by `MagicTabs`. */
|
|
27
|
+
icon: (props: MagicTabIconProps) => ReactNode;
|
|
28
|
+
/** Optional label shown while the tab is active. */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Resolved theme. Provided automatically by `MagicTabs`. */
|
|
31
|
+
theme: MagicTabBarTheme;
|
|
32
|
+
|
|
33
|
+
// ---- Injected by <TabTrigger asChild> ----
|
|
34
|
+
/** @internal */
|
|
35
|
+
isFocused?: boolean;
|
|
36
|
+
/** @internal */
|
|
37
|
+
onPress?: (event: GestureResponderEvent) => void;
|
|
38
|
+
/** @internal */
|
|
39
|
+
onLongPress?: (event: GestureResponderEvent) => void;
|
|
40
|
+
/** @internal */
|
|
41
|
+
href?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A single tab. Designed to be used as the `asChild` target of an
|
|
46
|
+
* Expo Router `<TabTrigger>`, which injects the focus state and press handlers.
|
|
47
|
+
*
|
|
48
|
+
* Each tab sizes to its content — just the icon when inactive, icon + label
|
|
49
|
+
* when active — so the active label is never clipped, on any screen width.
|
|
50
|
+
*/
|
|
51
|
+
export const MagicTabItem = forwardRef<RNView, MagicTabItemProps>(
|
|
52
|
+
function MagicTabItem({ icon, label, theme, isFocused, onPress, onLongPress }, ref) {
|
|
53
|
+
const focused = Boolean(isFocused);
|
|
54
|
+
const progress = useSharedValue(focused ? 1 : 0);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
progress.value = withSpring(focused ? 1 : 0, SPRING);
|
|
58
|
+
}, [focused, progress]);
|
|
59
|
+
|
|
60
|
+
const pillStyle = useAnimatedStyle(() => ({
|
|
61
|
+
opacity: progress.value,
|
|
62
|
+
transform: [{ scale: 0.8 + progress.value * 0.2 }],
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const color = focused ? theme.activeColor : theme.inactiveColor;
|
|
66
|
+
const showLabel = focused && !!label;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<AnimatedPressable
|
|
70
|
+
ref={ref}
|
|
71
|
+
onPress={onPress}
|
|
72
|
+
onLongPress={onLongPress}
|
|
73
|
+
accessibilityRole="tab"
|
|
74
|
+
accessibilityState={{ selected: focused }}
|
|
75
|
+
layout={transition}
|
|
76
|
+
style={[styles.pressable, focused && styles.pressableActive]}
|
|
77
|
+
>
|
|
78
|
+
<Animated.View
|
|
79
|
+
pointerEvents="none"
|
|
80
|
+
style={[
|
|
81
|
+
StyleSheet.absoluteFill,
|
|
82
|
+
{ backgroundColor: theme.activePillColor, borderRadius: theme.radius },
|
|
83
|
+
pillStyle,
|
|
84
|
+
]}
|
|
85
|
+
/>
|
|
86
|
+
{icon({ focused, color, size: theme.iconSize })}
|
|
87
|
+
{showLabel ? (
|
|
88
|
+
<Animated.Text
|
|
89
|
+
entering={FadeIn.duration(150)}
|
|
90
|
+
numberOfLines={1}
|
|
91
|
+
style={[styles.label, { color, fontSize: theme.fontSize }]}
|
|
92
|
+
>
|
|
93
|
+
{label}
|
|
94
|
+
</Animated.Text>
|
|
95
|
+
) : null}
|
|
96
|
+
</AnimatedPressable>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const styles = StyleSheet.create({
|
|
102
|
+
pressable: {
|
|
103
|
+
alignSelf: 'center',
|
|
104
|
+
flexDirection: 'row',
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
gap: 6,
|
|
108
|
+
paddingVertical: 8,
|
|
109
|
+
paddingHorizontal: 12,
|
|
110
|
+
},
|
|
111
|
+
// The active tab gets extra padding so its pill is a touch larger than the
|
|
112
|
+
// inactive icons, making the current tab easy to spot. flexShrink lets it
|
|
113
|
+
// truncate its label only as a last resort on very narrow screens.
|
|
114
|
+
pressableActive: {
|
|
115
|
+
flexShrink: 1,
|
|
116
|
+
paddingVertical: 10,
|
|
117
|
+
paddingHorizontal: 18,
|
|
118
|
+
},
|
|
119
|
+
label: {
|
|
120
|
+
fontWeight: '600',
|
|
121
|
+
flexShrink: 1,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Tabs, TabList, TabSlot, TabTrigger } from 'expo-router/ui';
|
|
3
|
+
import { MagicTabBar } from './MagicTabBar';
|
|
4
|
+
import { MagicTabItem } from './MagicTabItem';
|
|
5
|
+
import { defaultTabs } from './defaultTabs';
|
|
6
|
+
import { defaultTheme } from './theme';
|
|
7
|
+
import type { MagicTabBarTheme, MagicTabBarVariant, MagicTabConfig } from './types';
|
|
8
|
+
|
|
9
|
+
export interface MagicTabsProps {
|
|
10
|
+
/**
|
|
11
|
+
* The tabs to render, in order. Each entry maps a route to an icon + label.
|
|
12
|
+
* When omitted, a default set is used — Home, Explore, Notifications, Inbox
|
|
13
|
+
* and Profile — with matching Ionicons (see {@link defaultTabs}).
|
|
14
|
+
*/
|
|
15
|
+
tabs?: MagicTabConfig[];
|
|
16
|
+
/** Override any part of the default theme. */
|
|
17
|
+
theme?: Partial<MagicTabBarTheme>;
|
|
18
|
+
/** Position the bar floating over content (default) or docked in-flow. */
|
|
19
|
+
variant?: MagicTabBarVariant;
|
|
20
|
+
/**
|
|
21
|
+
* Make the bar background see-through. Off by default. When `true`, control
|
|
22
|
+
* the strength with `transparency`.
|
|
23
|
+
*/
|
|
24
|
+
isTransparent?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Opacity of the bar background while `isTransparent` is true, from 0 to 1
|
|
27
|
+
* (e.g. `0.4` = 40% visible). Clamped to a minimum so the bar never fully
|
|
28
|
+
* disappears. Defaults to 0.6.
|
|
29
|
+
*/
|
|
30
|
+
transparency?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Render the bar as native iOS Liquid Glass (via `expo-glass-effect`).
|
|
33
|
+
* Requires iOS 26+; on any other platform it falls back to the translucent
|
|
34
|
+
* `barColor`. No drop shadow is drawn in glass/transparent mode.
|
|
35
|
+
*/
|
|
36
|
+
glass?: boolean;
|
|
37
|
+
/** Render a custom background (e.g. a blur/glass view) behind the bar. */
|
|
38
|
+
renderBackground?: () => ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A drop-in custom tab bar for Expo Router.
|
|
43
|
+
*
|
|
44
|
+
* Use it in an `app/_layout.tsx` and pass your routes, icons and labels as props:
|
|
45
|
+
*
|
|
46
|
+
* ```tsx
|
|
47
|
+
* <MagicTabs
|
|
48
|
+
* tabs={[
|
|
49
|
+
* { name: 'index', href: '/', label: 'Home', icon: ({ color }) => <Home color={color} /> },
|
|
50
|
+
* { name: 'search', href: '/search', label: 'Search', icon: ({ color }) => <Search color={color} /> },
|
|
51
|
+
* ]}
|
|
52
|
+
* />
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function MagicTabs({
|
|
56
|
+
tabs = defaultTabs,
|
|
57
|
+
theme: themeOverride,
|
|
58
|
+
variant,
|
|
59
|
+
isTransparent,
|
|
60
|
+
transparency,
|
|
61
|
+
glass,
|
|
62
|
+
renderBackground,
|
|
63
|
+
}: MagicTabsProps) {
|
|
64
|
+
const theme: MagicTabBarTheme = { ...defaultTheme, ...themeOverride };
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Tabs>
|
|
68
|
+
<TabSlot />
|
|
69
|
+
<TabList asChild>
|
|
70
|
+
<MagicTabBar
|
|
71
|
+
theme={theme}
|
|
72
|
+
variant={variant}
|
|
73
|
+
isTransparent={isTransparent}
|
|
74
|
+
transparency={transparency}
|
|
75
|
+
glass={glass}
|
|
76
|
+
renderBackground={renderBackground}
|
|
77
|
+
>
|
|
78
|
+
{tabs.map((tab) => (
|
|
79
|
+
<TabTrigger key={tab.name} name={tab.name} href={tab.href} asChild>
|
|
80
|
+
<MagicTabItem icon={tab.icon} label={tab.label} theme={theme} />
|
|
81
|
+
</TabTrigger>
|
|
82
|
+
))}
|
|
83
|
+
</MagicTabBar>
|
|
84
|
+
</TabList>
|
|
85
|
+
</Tabs>
|
|
86
|
+
);
|
|
87
|
+
}
|