react-native-magic-tab-bar
v3.0.0
Published
Custom animated bottom tab bar for React Native — floating, glass/blur, badges & haptics. One component for Expo Router and React Navigation (bare RN CLI). iOS & Android, TypeScript.
Maintainers
Keywords
Readme
react-native-magic-tab-bar is a drop-in custom bottom tab bar for React Native. Instead of the default tab bar, you get a floating, animated bar with an active-tab pill, badges, labels, haptics, glass/blur backgrounds and full theming — on both iOS and Android. The same component works whether your app uses Expo Router or React Navigation (@react-navigation/bottom-tabs), so you can search less and ship faster.
Two entry points, one look:
- Expo Router →
MagicTabsfromreact-native-magic-tab-bar(see Quick start).- Bare React Native / React Navigation →
MagicTabBarNavigationfromreact-native-magic-tab-bar/react-navigation(see React Navigation).Pick whichever entry point matches your app — you never pull in the other framework's dependencies.
Features
- 🎯 Drop-in for both — one component for Expo Router and React Navigation; bring your own icons.
- 📐 Sized by its tabs — the bar hugs its content and centers, so two tabs give a compact pill, not a full-width bar with a hole in the middle.
- ✨ Animated active pill with a spring you can tune.
- 🏷️ Flexible labels — beside or below the icon; show on the active tab, always, or never.
- 🔴 Badges — dots or counts on any tab.
- 🧊 Glass / blur / transparent backgrounds (native iOS Liquid Glass supported).
- 📳 Haptics and press callbacks (great for "scroll to top" on re-press).
- ➕ Action (FAB) tab for a raised center button.
- 🪶 Light mode — a compact, icon-only bar you can switch on per tab (e.g. only on an immersive full-screen feed), with a smooth transition.
- ♿ Accessible by default — every tab is a labelled
tabin atablist, badges are announced ("Alerts, 3"), and touch targets meet the platform minimums. - 🎨 Fully themeable via a single
themeprop.
Table of contents
- Compatibility
- Why react-native-magic-tab-bar? (comparison)
- Installation
- Upgrading from 2.x
- Quick start (Expo Router)
- React Navigation (bare React Native)
- Recipes
- API
- FAQ
- Development
- Roadmap
- License
Screenshots
Compatibility
| Environment | How you use it | Supported |
| --- | --- | :---: |
| Expo Router (SDK 56+) | MagicTabs from react-native-magic-tab-bar | ✅ |
| React Navigation (@react-navigation/bottom-tabs v6 & v7) | MagicTabBarNavigation from react-native-magic-tab-bar/react-navigation | ✅ |
| Bare React Native CLI | via React Navigation | ✅ |
| Expo (managed & prebuild) | via Expo Router | ✅ |
| iOS · Android | — | ✅ |
| New Architecture (Fabric) | — | ✅ |
| TypeScript | types bundled, no @types needed | ✅ |
Why react-native-magic-tab-bar?
How it compares to other ways of building a React Native bottom tab bar:
| | Default @react-navigation/bottom-tabs | Roll your own tabBar | react-native-magic-tab-bar |
| --- | :---: | :---: | :---: |
| Floating / docked bar | ❌ | 🛠️ DIY | ✅ built-in |
| Animated active pill (Reanimated) | ❌ | 🛠️ DIY | ✅ |
| Badges, labels, haptics | partial | 🛠️ DIY | ✅ |
| Glass / blur / iOS Liquid Glass | ❌ | 🛠️ DIY | ✅ |
| Action / FAB center tab | ❌ | 🛠️ DIY | ✅ |
| Works with Expo Router and React Navigation | — | — | ✅ one component |
| Bring your own icons (no icon lib forced) | ✅ | ✅ | ✅ |
| TypeScript types bundled | ✅ | 🛠️ DIY | ✅ |
If you've searched for a custom bottom tab bar, floating tab bar, or an animated tab bar and found options that only support Expo or bare React Native — this is one drop-in component that covers both, so you don't rewrite the bar when you switch navigators.
Installation
npm install react-native-magic-tab-bar
# yarn add react-native-magic-tab-bar
# pnpm add react-native-magic-tab-bar
# bun add react-native-magic-tab-barThen install the peer dependencies for your framework 👇
Expo Router
Requirements: an Expo Router project (Expo SDK 56+) with an
app/directory.
Peer dependencies (normally already present in an Expo Router app):
npx expo install expo-router react-native-reanimated react-native-safe-area-context react-native-workletsMake sure the Reanimated/Worklets Babel plugin is enabled (Expo SDK 56's babel-preset-expo configures this automatically).
Optional — install only if you use the matching feature:
npx expo install expo-glass-effect # native iOS Liquid Glass (glass prop)
npx expo install expo-haptics # selection haptics (haptics prop)
npx expo install @expo/vector-icons # only for the /default-tabs demo setReact Native CLI (React Navigation)
Requirements: a bare React Native CLI app using
@react-navigation/bottom-tabs.
npm install @react-navigation/native @react-navigation/bottom-tabs \
react-native-screens react-native-safe-area-context \
react-native-reanimated react-native-workletsAdd the Worklets/Reanimated plugin last in your babel.config.js:
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-worklets/plugin'],
};iOS: cd ios && pod install. No Expo packages are required — the glass and haptics props are Expo-only and simply no-op here unless you install expo-glass-effect / expo-haptics.
See the runnable example-cli app for a complete setup.
Upgrading from 2.x
3.0 changes three visual defaults. Each has a one-line fix if you want the old behavior:
- The bar is now content-sized and centered by default (see Bar width). To keep an edge-to-edge bar, add
fullWidth:<MagicTabs tabs={tabs} fullWidth /> dockedbars now add the safe-area bottom inset themselves. If you wrapped a docked bar in a bottom-edgeSafeAreaViewto lift it off the home indicator, remove that wrapper to avoid doubled spacing.renderBackgroundno longer casts a drop shadow (a custom background — usually a blur view — provides its own fill). No action needed unless you relied on the shadow behind an opaque custom background.
See the CHANGELOG for the full list.
Quick start
Use MagicTabs as your tab navigator in app/_layout.tsx. Each entry in tabs maps a route to an icon and label:
// app/_layout.tsx
import { MagicTabs } from "react-native-magic-tab-bar";
import { Ionicons } from "@expo/vector-icons";
export default function Layout() {
return (
<MagicTabs
tabs={[
{ name: "index", href: "/", label: "Home", icon: ({ color, size }) => <Ionicons name="home" color={color} size={size} /> },
{ name: "search", href: "/search", label: "Search", icon: ({ color, size }) => <Ionicons name="search" color={color} size={size} /> },
{ name: "profile", href: "/profile", label: "Profile", icon: ({ color, size }) => <Ionicons name="person" color={color} size={size} /> },
]}
/>
);
}namemust match the route file in yourapp/directory (e.g.index,search,profile).hrefis where the tab navigates.iconreceives{ focused, color, size }so you can swap glyphs/colors for the active state.
MagicTabsrenders Expo Router's<Tabs>for you — put it straight inapp/_layout.tsx. There's no separate<Tabs>wrapper to set up.
Tip — filled vs. outline icons: use focused to swap the glyph on the active tab:
icon: ({ focused, color, size }) => (
<Ionicons name={focused ? "home" : "home-outline"} color={color} size={size} />
);Want a ready-made set for a quick look? Import the demo tabs from the subpath (this is the only thing that pulls in
@expo/vector-icons, so the core stays dependency-free):import { defaultTabs } from "react-native-magic-tab-bar/default-tabs"; <MagicTabs tabs={defaultTabs} />;
React Navigation (bare React Native)
In a bare React Native app you use MagicTabBarNavigation as the tabBar of a
bottom tab navigator.
It takes the same tab config as MagicTabs — just without href (React
Navigation routes by name) — so the bar looks and behaves identically.
import { NavigationContainer } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { MagicTabBarNavigation } from "react-native-magic-tab-bar/react-navigation";
import Ionicons from "react-native-vector-icons/Ionicons";
const Tab = createBottomTabNavigator();
const tabs = [
{ name: "Home", label: "Home", icon: ({ color, size }) => <Ionicons name="home" color={color} size={size} /> },
{ name: "Search", label: "Search", icon: ({ color, size }) => <Ionicons name="search" color={color} size={size} /> },
{ name: "Profile", label: "Profile", icon: ({ color, size }) => <Ionicons name="person" color={color} size={size} /> },
];
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Tab.Navigator
screenOptions={{ headerShown: false }}
tabBar={(props) => <MagicTabBarNavigation {...props} tabs={tabs} />}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}- Each
tabs[].namemust match a<Tab.Screen name>. - No
href— that's an Expo Router concept; React Navigation navigates byname. - Every visual prop from
MagicTabs(theme,showLabels,labelPosition,variant,fullWidth,isTransparent,renderBackground,haptics,onTabPress, …) works here too, plus all the Recipes below.
Icons are bring-your-own, exactly like the Expo entry — the
iconrender function is identical. This example usesreact-native-vector-icons(the bare-RN counterpart of@expo/vector-icons). Bundle its fonts the CocoaPods way — notreact-native-asset: on iOS the pod bundles the font files (just add aUIAppFontsentry toInfo.plist), and on Android apply the package'sfonts.gradleinapp/build.gradle. The only Expo-specific props areglass(expo-glass-effect) andhaptics(expo-haptics), which no-op in bare RN unless you add those modules.
Prefer to keep the config next to your screens instead of a
tabsarray? The config is just data — build it however you like; onlynameandiconare required per tab.
Recipes
The recipes below are written with
MagicTabs(Expo Router), but every prop shown also works onMagicTabBarNavigation— pass it the same way.
Bar width
The bar is sized by its tabs and centered — it grows with the number of tabs
(and the active tab's label) up to the width left by theme.horizontalMargin,
then stops. A two-tab bar is a compact pill rather than an edge-to-edge bar with
a large gap in the middle. The width change is animated, so adding or removing a
tab morphs the bar.
<MagicTabs tabs={tabs} /> // content-sized, centered (default)
<MagicTabs tabs={tabs} fullWidth /> // edge-to-edge, tabs spread evenlyUse theme.horizontalMargin to cap how wide it may ever get:
<MagicTabs tabs={tabs} theme={{ horizontalMargin: 48 }} /> // narrower ceilingLabels
Labels are beside the icon by default (labelPosition="right"), shown only on the active tab.
<MagicTabs tabs={tabs} labelPosition="bottom" showLabels="always" /> // Material-style bar
<MagicTabs tabs={tabs} showLabels={false} /> // icon-only bar| showLabels | Effect |
| --- | --- |
| true / "active" | Label on the focused tab only (default) |
| "always" | Label on every tab (requires labelPosition="bottom") |
| false / "never" | Icon-only |
labelPosition is "right" (default) or "bottom". You can also override visibility per tab with showLabel on the tab config.
Badges
{ name: "inbox", href: "/inbox", label: "Inbox", badge: 5, icon }, // count bubble
{ name: "alerts", href: "/alerts", label: "Alerts", badge: true, icon }, // dotbadge accepts a number, string, or boolean (true = dot). Numbers above 99 show as 99+. Colors come from theme.badgeColor / theme.badgeTextColor.
Haptics and press callbacks
<MagicTabs
tabs={tabs}
haptics // selection haptic on tap (needs expo-haptics)
onTabPress={(name, focused) => {
if (focused) scrollToTop(name); // re-pressing the active tab
}}
/>onTabPress / onTabLongPress receive (name, focused), where focused tells you the tab was already active — perfect for scroll-to-top or reset-stack behavior.
Disabled tabs
{ name: "soon", href: "/soon", label: "Soon", disabled: true, icon }Dims the tab and blocks navigation to it.
Action (FAB) tab
Render a raised, circular center button:
{ name: "create", href: "/create", variant: "action", icon }Uses theme.actionColor / theme.actionIconColor.
Glass, blur and transparency
<MagicTabs tabs={tabs} glass /> // native iOS Liquid Glass (iOS 26+)
<MagicTabs tabs={tabs} isTransparent transparency={0.4} />// translucent bar
<MagicTabs tabs={tabs} renderBackground={() => <MyBlurView />} /> // any custom backgroundglassusesexpo-glass-effecton iOS 26+ and falls back to the translucentbarColorelsewhere.renderBackgroundrenders any view behind the bar; when omitted, a solidbarColoris used.
Light mode (compact bar)
A shorter, icon-only bar that floats higher and — like the normal bar — is sized by its tabs, so it stays a tight pill whatever the tab count. Turn it on for the whole bar, or per tab so it only appears on an immersive screen:
// Whole bar
<MagicTabs tabs={tabs} isLight lightBottomMargin={40} />
// Per tab — the bar morphs to light only while "explore" is the active route
<MagicTabs
tabs={[
{ name: "index", href: "/", label: "Home", icon },
{ name: "explore", href: "/explore", label: "Explore", icon, isLight: true },
{ name: "profile", href: "/profile", label: "Profile", icon },
]}
/>The transition between the normal and light bar is animated. lightBottomMargin (default 14) controls the extra gap above the screen edge in light mode.
Theming
Everything visual — colors, sizes, corner radius, the animation spring — lives in one theme prop. Override any subset; the rest falls back to defaultTheme.
The colors that matter most:
| Token | Controls |
| --- | --- |
| barColor | the bar's background |
| activePillColor | the pill highlight behind the active tab |
| activeColor | the active tab's icon + label |
| inactiveColor | the icons of inactive tabs |
| badgeColor / badgeTextColor | the badge bubble + its text |
| actionColor / actionIconColor | the action (FAB) tab |
<MagicTabs
tabs={tabs}
theme={{
barColor: "#17171C", // dark bar
activePillColor: "#2563EB", // blue highlight behind the active tab
activeColor: "#FFFFFF", // active icon/label
inactiveColor: "#9BA0AA", // inactive icons
radius: 24, // rounder bar & pill
spring: { mass: 0.6, damping: 16, stiffness: 200 }, // snappier animation
}}
/>See the full token list with defaults for sizes (iconSize, height, fontSize), spacing (horizontalMargin, bottomInset) and more.
Accessibility
The bar is usable with VoiceOver and TalkBack out of the box — there is nothing to switch on.
Tabs are labelled. Each tab exposes its label as its accessible name,
falling back to the route name when a tab has no label. The row is marked
accessibilityRole="tablist" and each tab accessibilityRole="tab", so
assistive tech announces "tab 2 of 5" rather than reading five anonymous
buttons. The active tab is reported as selected.
Badges are announced. A badge is invisible to a screen reader, so its
meaning is folded into the tab's accessible name — a count or string badge
appends its value, and a dot badge (badge: true) announces "new item":
{ name: "alerts", href: "/alerts", label: "Alerts", badge: 3, icon } // → "Alerts, 3"
{ name: "alerts", href: "/alerts", label: "Alerts", badge: 150, icon } // → "Alerts, 99+"
{ name: "feed", href: "/feed", label: "Feed", badge: true, icon } // → "Feed, new item"Touch targets meet the platform minimums. Icons render smaller than the
~44pt (iOS HIG) / 48dp (Material) minimum, so tabs add vertical hitSlop to
reach it without changing the visual layout. The extra area is vertical only, so
neighbouring tabs never overlap.
Large text is respected, within limits. Labels and badge counts scale with the OS font size, capped so they can't blow out the bar's fixed height or overflow the badge bubble. Labels truncate to one line rather than wrapping.
Targeting tabs from an e2e suite? Prefer
testIDover the accessible name — the name is the visible label, which may collide with other text on screen.
API
<MagicTabs />
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| tabs | MagicTabConfig[] | required | The tabs, in order. |
| theme | Partial<MagicTabBarTheme> | defaultTheme | Override any visual token. |
| showLabels | boolean \| 'active' \| 'always' \| 'never' | 'active' | When labels are shown. 'always' needs labelPosition="bottom". |
| labelPosition | 'right' \| 'bottom' | 'right' | Label beside or below the icon. |
| variant | 'floating' \| 'docked' | 'floating' | Float over content, or dock in flow. |
| fullWidth | boolean | false | Stretch the bar edge-to-edge and spread the tabs. Off by default — the bar is sized by its tabs. |
| isLight | boolean | false | Force the compact light bar for all tabs. |
| lightBottomMargin | number | 14 | Extra bottom gap in light mode only. |
| isTransparent | boolean | false | Make the bar background see-through. |
| transparency | number | 0.6 | Bar opacity 0–1 while isTransparent. |
| glass | boolean | false | Native iOS Liquid Glass (needs expo-glass-effect). |
| renderBackground | () => ReactNode | — | Custom background (blur/glass) behind the bar. |
| haptics | boolean | false | Selection haptic on press (needs expo-haptics). |
| onTabPress | (name, focused) => void | — | Fired on tab press. |
| onTabLongPress | (name, focused) => void | — | Fired on tab long-press. |
MagicTabConfig
| Field | Type | Description |
| --- | --- | --- |
| name | string | Route name. Expo Router: matches the file in app/. React Navigation: matches <Tab.Screen name>. |
| href | MagicHref? | Destination, e.g. / or /search. Required for Expo Router, ignored by React Navigation. |
| icon | (p: MagicTabIconProps) => ReactNode | Renders the icon. Gets { focused, color, size }. |
| label | string? | Text label. |
| showLabel | boolean? | Per-tab override of showLabels. |
| badge | number \| string \| boolean? | Dot (true) or count bubble. |
| disabled | boolean? | Dim the tab and block navigation. |
| variant | 'action'? | Render as a raised FAB button. |
| isLight | boolean? | Switch the whole bar to light mode while this tab is active. |
| testID | string? | testID for the tab, so e2e suites (Maestro, Detox, Appium…) can target it. Without one, tabs are reachable by their accessibility label — which is the visible label, and may collide with other text on screen. |
<MagicTabBarNavigation />
The React Navigation binding, imported from react-native-magic-tab-bar/react-navigation. Pass it to Tab.Navigator's tabBar prop.
It receives React Navigation's BottomTabBarProps (spread from {...props}) plus:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| tabs | MagicNavigationTab[] | required | Per-tab config keyed by route name (same as MagicTabConfig but without href). |
All other visual props are identical to <MagicTabs />: theme, showLabels, labelPosition, variant, fullWidth, isLight, lightBottomMargin, isTransparent, transparency, glass, renderBackground, haptics, onTabPress, onTabLongPress.
MagicNavigationTabisOmit<MagicTabConfig, 'href'>.
MagicTabBarTheme
| Token | Default | |
| --- | --- | --- |
| barColor | rgba(38,38,40,0.94) | Bar background |
| activePillColor | rgba(120,120,124,0.55) | Active-tab pill |
| activeColor | #FFFFFF | Active icon/label color |
| inactiveColor | #FFFFFF | Inactive icon color |
| iconSize | 22 | Icon size |
| fontSize | 12 | Active label size |
| height | 56 | Bar height |
| radius | 28 | Bar & pill corner radius |
| badgeColor | #FF3B30 | Badge background |
| badgeTextColor | #FFFFFF | Badge text |
| actionColor | #0A84FF | Action (FAB) background |
| actionIconColor | #FFFFFF | Action (FAB) icon |
| horizontalMargin | 14 | Side margin from screen edges |
| bottomInset | 10 | Extra space below the bar |
| spring | { mass: 0.6, damping: 18, stiffness: 180 } | Pill/label animation |
Exported types
MagicTabConfig, MagicHref, MagicTabIconProps, MagicTabBarTheme, MagicTabBarVariant, MagicTabPressHandler, MagicLabelMode, MagicLabelPosition, MagicSpringConfig, plus the defaultTheme value.
From react-native-magic-tab-bar/react-navigation: MagicTabBarNavigation, MagicTabBarNavigationProps, MagicNavigationTab.
defaultTabsis exported from thereact-native-magic-tab-bar/default-tabssubpath (not the main entry), so the core adds zero runtime dependencies.
FAQ
How do I create a custom bottom tab bar in Expo Router?
Render MagicTabs in your app/_layout.tsx and pass a tabs array mapping each route to an icon and label. It replaces Expo Router's default tab bar without you writing any layout code — see the Quick start.
How do I replace the default tab bar in React Navigation?
Pass MagicTabBarNavigation to your navigator's tabBar prop: <Tab.Navigator tabBar={(props) => <MagicTabBarNavigation {...props} tabs={tabs} />}>. It works with @react-navigation/bottom-tabs v6 and v7 in a bare React Native CLI app — see React Navigation (bare React Native).
Does it work with both Expo Router and React Navigation?
Yes — that's the point. The same look ships as two entry points: MagicTabs for Expo Router and react-native-magic-tab-bar/react-navigation for bare React Native. You only pull in the framework you actually use; the other framework's dependencies are never installed.
Does it support the New Architecture (Fabric) and Reanimated 3?
Yes. Animations run on react-native-reanimated (v3+) with react-native-worklets, and the library is tested on the New Architecture (Fabric) on both iOS and Android.
How do I add a floating action button (FAB) to the tab bar?
Give any tab variant: "action" and it renders as a raised, circular center button using the theme's actionColor / actionIconColor — see Action (FAB) tab.
Does it support iOS Liquid Glass / a blur tab bar?
Yes. Turn on the glass prop for a translucent blur background, backed by native iOS Liquid Glass when expo-glass-effect is installed — see Glass, blur and transparency. On Android and bare RN it falls back gracefully.
Do I have to use a specific icon library?
No. Bring your own icons — pass any component from each tab's icon render function (@expo/vector-icons, react-native-vector-icons, SVGs, or your own). The core adds zero runtime dependencies and forces no icon library.
Is it written in TypeScript?
Yes — types are bundled, so there's no separate @types package to install.
Development
This repo is a monorepo: the library lives at the root (src/), with two runnable example apps that import the library straight from source:
example— Expo Router app (MagicTabs).example-cli— bare React Native CLI app (MagicTabBarNavigation).
# Expo Router example
npm install # from the repo root: installs example deps + build tooling
cd example && npx expo start
# React Native CLI example
cd example-cli && npm install
npm run ios # or: npm run androidEditing files in src/ hot-reloads in whichever example is running. (If a
library edit doesn't show up, restart Metro with npx expo start --clear — a
stale bundler cache serves the old library code.)
Tests
npm test # unit tests: the pure logic in src/utils.ts (no RN runtime)
npm run typecheck
npm run e2e # Maestro flows against the Expo example on a simulator
npm run e2e:measure # asserts the bar's on-device geometry at every tab countThe Maestro suite is what covers the parts a unit test can't: it
drives the example app at 1–5 tabs and checks the bar is content-sized, centred,
and never wider than theme.horizontalMargin allows. See
.maestro/README.md for setup and what each flow covers.
Building / publishing
Publishing is restricted to package maintainers.
npm run build # react-native-builder-bob -> lib/ (ESM + d.ts)
npm publishRoadmap
- Sliding active-pill that animates between tabs (currently per-tab pill).
- ✅ ~~A React Navigation (
@react-navigation/bottom-tabs) adapter for bare RN.~~ Shipped — see React Navigation.
License
MIT
