react-native-radial-tabs
v0.1.1
Published
An accessible React Native tab bar that branches into a radial (pie) menu on long-press. Fit deep tab trees into a tiny footprint — hold a tab, swipe to a sub-option, release to pick.
Maintainers
Readme
react-native-radial-tabs
An accessible React Native tab bar that branches into a radial menu on long-press. Fit a deep tab tree into a tiny footprint: tap a tab to switch, press & hold a tab to bloom its options in a semicircle above your thumb, swipe to one and release to pick. Deep trees support drill-down.
- 📱 Built for phones — Expo / React Native, iOS + Android
- 🌳 Arbitrary tab trees (
children, nested as deep as you like) - 🎨 Themeable via a plain JS
themeobject + render-slot props - ♿ Accessible — VoiceOver/TalkBack get a navigable list menu fallback; honors reduce-motion
- ⚡ 60fps gestures on the UI thread (Reanimated + Gesture Handler)
Ported from a web prototype (kept in
reference/). This package is the React Native version.
See it in action
Screenshots rendered from the interaction prototype in reference/; the React Native component renders the same UI. Run it live on your phone via the example app.
Install
npm install react-native-radial-tabs
# peer deps (Expo: use `npx expo install`)
npx expo install react-native-gesture-handler react-native-reanimatedSetup (once)
- Add the Reanimated Babel plugin (must be last) in
babel.config.js:plugins: ["react-native-reanimated/plugin"] - Wrap your app in
GestureHandlerRootViewandRadialTabsProvider:
The provider hosts the full-screen overlay so the menu can paint above everything.import { GestureHandlerRootView } from "react-native-gesture-handler"; import { RadialTabsProvider } from "react-native-radial-tabs"; export default function App() { return ( <GestureHandlerRootView style={{ flex: 1 }}> <RadialTabsProvider> <YourApp /> </RadialTabsProvider> </GestureHandlerRootView> ); }
Usage
import { Text } from "react-native";
import { RadialTabs, type RadialNode } from "react-native-radial-tabs";
const tabs: RadialNode[] = [
{ id: "home", label: "Home", icon: <Text>🏠</Text> },
{
id: "create",
label: "Create",
icon: <Text>➕</Text>,
children: [
{ id: "photo", label: "Photo", icon: <Text>📷</Text> },
{ id: "note", label: "Note", icon: <Text>📝</Text> },
// branches can nest — release-to-select becomes drill-down automatically
{
id: "doc",
label: "Doc",
children: [
{ id: "sheet", label: "Sheet" },
{ id: "slides", label: "Slides" },
],
},
],
},
];
<RadialTabs
items={tabs}
onSelect={(node, info) => {
// info.path = full trail, info.type = "tab" | "leaf" | "branch"
console.log(info.path.map((n) => n.label).join(" › "));
}}
/>;Dock it at the bottom of your screen like any tab bar.
Data model
interface RadialNode {
id: string;
label: string;
icon?: ReactNode; // any element (vector icon, <Text>emoji</Text>, image)
children?: RadialNode[]; // presence makes it a branch (hold to open / drill)
data?: unknown; // your payload, returned on select
accessibilityLabel?: string;
}Props (highlights)
| Prop | Default | Notes |
| --- | --- | --- |
| items | — | The tab tree. |
| value / defaultValue | first tab | Active tab id (controlled / uncontrolled). |
| onSelect(node, info) | — | info = { path, tabIndex, type }. |
| arc | "semi" | "semi" (180°) · "quarter" (90°) · "full" (360°). |
| mode | "auto" | "select" (one level) · "drill" (multi-level) · "auto" (drill when nested). |
| longPressMs | 220 | Hold time before bloom. |
| dwellMs | 320 | Rest time to drill in / go back. |
| deadZone | 30 | Center radius that cancels. |
| maxRadius | 150 | Fan radius cap (scales down on small screens). |
| theme | dark | Partial<RadialTheme> merged over defaults. |
| renderTab / renderItem / renderCenter | — | Full custom content slots. |
| style / barStyle / tabStyle / itemStyle | — | Style overrides. |
Theming
No CSS — a plain object merged over DEFAULT_THEME. Set app-wide defaults on the
provider, override per instance with the theme prop:
<RadialTabsProvider theme={{ accent: "#ff6b6b" }}>…</RadialTabsProvider>
<RadialTabs theme={{ itemBackground: "#1b1f2e", barBorderRadius: 28, itemSize: 72 }} ... />Keys: accent, accentText, barBackground, barBorderRadius, tabColor,
tabColorActive, itemBackground, itemColor, overlayBackground, ringColor,
centerBackground, centerBorder, centerColor, guideLineColor, itemSize,
shadowColor.
For total control, use the render slots (renderTab, renderItem, renderCenter).
Accessibility
- Tabs are a proper
tablist/tabwith selected state, labels and hints. - When a screen reader is active, the swipe wheel (not operable by VoiceOver/TalkBack) is replaced by an equivalent navigable list menu that walks the whole tree.
- Reduce motion is honored (animations are skipped).
Run the example
cd example
npm install
npx expo start # open in Expo GoLicense
MIT
