diff --git a/.changeset/panel-disable-support.md b/.changeset/panel-disable-support.md
new file mode 100644
index 0000000000..ef5f8fb75f
--- /dev/null
+++ b/.changeset/panel-disable-support.md
@@ -0,0 +1,6 @@
+---
+'@storybook/react-native-ui': minor
+'@storybook/react-native-ui-lite': minor
+---
+
+feat: honor `parameters[paramKey].disable` on addon panels — matches web Storybook. When every panel is disabled for the current story, the addons UI is hidden.
diff --git a/docs/docs/intro/addons/index.md b/docs/docs/intro/addons/index.md
index 53ca6b9304..98b4c625fe 100644
--- a/docs/docs/intro/addons/index.md
+++ b/docs/docs/intro/addons/index.md
@@ -35,8 +35,30 @@ export default main;
On-device addons contain React Native code that can only run on the device. When listed in the regular `addons` array, Storybook Core tries to evaluate them as presets during server-side operations (like `extract` or `build`), which fails because Node.js can't load React Native modules. The `deviceAddons` property ensures they're only loaded at runtime on the device.
For backwards compatibility, on-device addons in the `addons` array still work — they're detected by the "ondevice" substring in their name and handled correctly. However, `deviceAddons` is the recommended approach.
+
:::
+## Hiding addon panels per story
+
+On-device addon panels can be hidden for a story with the same `parameters[addonKey].disable` convention used by Storybook web. When every registered panel is disabled for the selected story, the addons button and panel are hidden.
+
+```ts
+export const WithoutControls = {
+ parameters: {
+ controls: { disable: true },
+ },
+};
+
+export const WithoutAnyAddonPanels = {
+ parameters: {
+ actions: { disable: true },
+ backgrounds: { disable: true },
+ controls: { disable: true },
+ notes: { disable: true },
+ },
+};
+```
+
## Actions
The Actions addon lets you log events and actions inside your stories. It's useful for verifying component interactions and event handling.
diff --git a/docs/docs/intro/writing-stories.md b/docs/docs/intro/writing-stories.md
index 9baa9df1e3..9d3073830c 100644
--- a/docs/docs/intro/writing-stories.md
+++ b/docs/docs/intro/writing-stories.md
@@ -144,6 +144,8 @@ React Native Storybook provides several built-in parameters to control the on-de
| `hideFullScreenButton` | `boolean` | When `true`, hides the fullscreen toggle button |
| `layout` | `'padded'` \| `'centered'` \| `'fullscreen'` | Controls the layout of the story container. `'padded'` adds padding, `'centered'` centers the content, `'fullscreen'` removes any default spacing |
+On-device addon panels can also be hidden per story with `parameters[addonKey].disable`. This follows the same convention as Storybook web. For example, `parameters: { controls: { disable: true } }` hides the Controls panel for that story. When every addon panel is disabled for the selected story, the addons UI is hidden.
+
```tsx
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-native';
@@ -186,6 +188,16 @@ export const EdgeToEdge: Story = {
noSafeArea: true,
},
};
+
+// Story without the Controls addon panel
+export const WithoutControls: Story = {
+ args: {
+ label: 'Button',
+ },
+ parameters: {
+ controls: { disable: true },
+ },
+};
```
### Using decorators
diff --git a/examples/expo-example/.rnstorybook/storybook.requires.ts b/examples/expo-example/.rnstorybook/storybook.requires.ts
index 6e5a4cada9..fdd434d6cc 100644
--- a/examples/expo-example/.rnstorybook/storybook.requires.ts
+++ b/examples/expo-example/.rnstorybook/storybook.requires.ts
@@ -1,6 +1,6 @@
/* do not change this file, it is auto generated by storybook. */
///
-import { start, updateView, View, type Features } from '@storybook/react-native';
+import { start, updateView, type View, type Features } from '@storybook/react-native';
import "storybook-addon-deep-controls/register";
@@ -65,7 +65,7 @@ const annotations = [
globalThis.STORIES = normalizedStories;
globalThis.STORYBOOK_WEBSOCKET = {
- host: '192.168.1.171',
+ host: '192.168.1.210',
port: 7007,
secured: false,
};
diff --git a/examples/expo-example/components/AddonPanelDisable/AddonPanelDisable.stories.tsx b/examples/expo-example/components/AddonPanelDisable/AddonPanelDisable.stories.tsx
new file mode 100644
index 0000000000..90f1e1b42f
--- /dev/null
+++ b/examples/expo-example/components/AddonPanelDisable/AddonPanelDisable.stories.tsx
@@ -0,0 +1,137 @@
+import type { Meta, StoryObj } from '@storybook/react-native';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { fn } from 'storybook/test';
+
+type AddonPanelDisableDemoProps = {
+ label: string;
+ count: number;
+ enabled: boolean;
+ onPress: () => void;
+};
+
+const AddonPanelDisableDemo = ({ label, count, enabled, onPress }: AddonPanelDisableDemoProps) => (
+
+ {label}
+ Count: {count}
+ Enabled: {enabled ? 'true' : 'false'}
+ [styles.button, pressed && styles.buttonPressed]}
+ onPress={onPress}
+ >
+ Log action
+
+
+);
+
+const meta = {
+ title: 'AddonPanelDisable',
+ component: AddonPanelDisableDemo,
+ args: {
+ label: 'Addon panel disable test',
+ count: 1,
+ enabled: true,
+ onPress: fn(),
+ },
+ argTypes: {
+ label: { control: 'text' },
+ count: { control: 'number' },
+ enabled: { control: 'boolean' },
+ },
+ parameters: {
+ notes: `
+# Addon panel disable test
+
+Use these stories to manually verify \`parameters[paramKey].disable\`.
+`,
+ backgrounds: {
+ options: {
+ panelDisableLight: { name: 'Panel disable light', value: '#f7f2e8' },
+ panelDisableDark: { name: 'Panel disable dark', value: '#20232a' },
+ },
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const AllPanelsEnabled: Story = {};
+
+export const ControlsDisabled: Story = {
+ parameters: {
+ controls: { disable: true },
+ },
+};
+
+export const ActionsDisabled: Story = {
+ parameters: {
+ actions: { disable: true },
+ },
+};
+
+export const BackgroundsDisabled: Story = {
+ parameters: {
+ backgrounds: { disable: true },
+ },
+};
+
+export const NotesDisabled: Story = {
+ parameters: {
+ notes: { disable: true },
+ },
+};
+
+export const FirstPanelDisabled: Story = {
+ parameters: {
+ controls: { disable: true },
+ },
+};
+
+export const LastPanelOnly: Story = {
+ parameters: {
+ actions: { disable: true },
+ backgrounds: { disable: true },
+ controls: { disable: true },
+ },
+};
+
+export const AllPanelsDisabled: Story = {
+ parameters: {
+ actions: { disable: true },
+ backgrounds: { disable: true },
+ controls: { disable: true },
+ notes: { disable: true },
+ },
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 12,
+ padding: 24,
+ },
+ heading: {
+ fontSize: 20,
+ fontWeight: '700',
+ textAlign: 'center',
+ },
+ body: {
+ fontSize: 16,
+ },
+ button: {
+ backgroundColor: '#1ea7fd',
+ borderRadius: 4,
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ },
+ buttonText: {
+ color: '#ffffff',
+ fontWeight: '700',
+ },
+ buttonPressed: {
+ opacity: 0.8,
+ },
+});
diff --git a/packages/react-native-ui-lite/src/Layout.tsx b/packages/react-native-ui-lite/src/Layout.tsx
index 4580f83b2d..d62502f512 100644
--- a/packages/react-native-ui-lite/src/Layout.tsx
+++ b/packages/react-native-ui-lite/src/Layout.tsx
@@ -11,12 +11,25 @@ import {
useStoreNumberState,
useStyle,
} from '@storybook/react-native-ui-common';
-import { ReactElement, ReactNode, useCallback, useLayoutEffect, useRef, useState } from 'react';
+import {
+ ReactElement,
+ ReactNode,
+ useCallback,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { Text, TouchableOpacity, useWindowDimensions, View, ViewStyle } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { SET_CURRENT_STORY } from 'storybook/internal/core-events';
import type { Args, StoryContext } from 'storybook/internal/csf';
-import { type API_IndexHash } from 'storybook/internal/types';
+import {
+ Addon_TypesEnum,
+ type Addon_BaseType,
+ type Addon_Collection,
+ type API_IndexHash,
+} from 'storybook/internal/types';
import { addons } from 'storybook/manager-api';
import { AddonsTabs, MobileAddonsPanel, MobileAddonsPanelRef } from './MobileAddonsPanel';
import { MobileMenuDrawer, MobileMenuDrawerRef } from './MobileMenuDrawer';
@@ -119,6 +132,14 @@ export const Layout = ({
'desktopPanelState',
true
);
+
+ const hasEnabledPanels = useMemo(() => {
+ const allPanels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL);
+ return Object.values(allPanels).some(
+ (p) => !p.paramKey || !story?.parameters?.[p.paramKey]?.disable
+ );
+ }, [story?.parameters]);
+
const [isMobileSearchActive, setIsMobileSearchActive] = useState(false);
const [sidebarWidth, setSidebarWidth] = useStoreNumberState('desktopSidebarWidth', 240);
@@ -317,7 +338,7 @@ export const Layout = ({
)}
- {isDesktop ? (
+ {isDesktop && hasEnabledPanels ? (
<>
{desktopAddonsPanelOpen ? (
{desktopAddonsPanelOpen ? (
- setDesktopAddonsPanelOpen(false)} />
+ setDesktopAddonsPanelOpen(false)}
+ />
) : (
- addonPanelRef.current.setAddonsPanelOpen(true)}
- Icon={BottomBarToggleIcon}
- accessibilityLabel="Open addons panel"
- />
+ {hasEnabledPanels && (
+ addonPanelRef.current.setAddonsPanelOpen(true)}
+ Icon={BottomBarToggleIcon}
+ accessibilityLabel="Open addons panel"
+ />
+ )}
) : null}
@@ -395,7 +422,9 @@ export const Layout = ({
)}
- {isDesktop ? null : }
+ {!isDesktop && hasEnabledPanels ? (
+
+ ) : null}
);
};
diff --git a/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx b/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx
index abc7934553..500725bc04 100644
--- a/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx
+++ b/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx
@@ -1,5 +1,6 @@
import { styled, useTheme } from '@storybook/react-native-theming';
import { IconButton, useStyle } from '@storybook/react-native-ui-common';
+import type { Parameters } from 'storybook/internal/csf';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react';
import {
Animated,
@@ -28,8 +29,10 @@ export interface MobileAddonsPanelRef {
setAddonsPanelOpen: (isOpen: boolean) => void;
}
-export const MobileAddonsPanel = forwardRef(
- ({ storyId }, ref) => {
+type MobileAddonsPanelProps = { storyId?: string; parameters?: Parameters };
+
+export const MobileAddonsPanel = forwardRef(
+ ({ storyId, parameters }, ref) => {
const theme = useTheme();
const { height } = useWindowDimensions();
const defaultPanelHeight = height / 2;
@@ -145,6 +148,7 @@ export const MobileAddonsPanel = forwardRef
@@ -191,12 +195,25 @@ const hiddenStyle = {
const hitSlop = { top: 10, right: 10, bottom: 10, left: 10 };
-export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId?: string }) => {
- const panels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL);
+type AddonsTabsProps = {
+ onClose?: () => void;
+ storyId?: string;
+ parameters?: Parameters;
+};
+
+export const AddonsTabs = ({ onClose, storyId, parameters }: AddonsTabsProps) => {
+ const panels = useMemo>(() => {
+ const allPanels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL);
+
+ return Object.fromEntries(
+ Object.entries(allPanels).filter(([, p]) => !p.paramKey || !parameters?.[p.paramKey]?.disable)
+ );
+ }, [parameters]);
+
const insets = useSafeAreaInsets();
const [addonSelected, setAddonSelected] = useState(Object.keys(panels)[0]);
-
const panelEntries = useMemo(() => Object.entries(panels), [panels]);
+ const activeAddonId = panels[addonSelected] ? addonSelected : panelEntries[0]?.[0];
const scrollContentContainerStyle = useStyle(
() => ({
@@ -220,7 +237,7 @@ export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId
return (
setAddonSelected(id)}
text={String(resolvedTitle)}
/>
@@ -252,7 +269,7 @@ export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId
) : (
panelEntries.map(([id, p]) => (
-
+
))
diff --git a/packages/react-native-ui/src/Layout.tsx b/packages/react-native-ui/src/Layout.tsx
index ae808b9160..f83b705b68 100644
--- a/packages/react-native-ui/src/Layout.tsx
+++ b/packages/react-native-ui/src/Layout.tsx
@@ -11,13 +11,18 @@ import {
useStyle,
type SBUI,
} from '@storybook/react-native-ui-common';
-import { ReactNode, useCallback, useLayoutEffect, useRef, useState } from 'react';
+import { ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { ScrollView, Text, TouchableOpacity, View, ViewStyle } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { SET_CURRENT_STORY } from 'storybook/internal/core-events';
import type { Args, StoryContext } from 'storybook/internal/csf';
-import type { API_IndexHash } from 'storybook/internal/types';
+import {
+ Addon_TypesEnum,
+ type Addon_BaseType,
+ type Addon_Collection,
+ type API_IndexHash,
+} from 'storybook/internal/types';
import { addons } from 'storybook/manager-api';
import { DEFAULT_REF_ID } from './constants';
import { BottomBarToggleIcon } from './icon/BottomBarToggleIcon';
@@ -117,6 +122,14 @@ export const Layout = ({
true
);
+ const hasEnabledPanels = useMemo(() => {
+ const allPanels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL);
+
+ return Object.values(allPanels).some(
+ (p) => !p.paramKey || !story?.parameters?.[p.paramKey]?.disable
+ );
+ }, [story?.parameters]);
+
const [uiHidden, setUiHidden] = useState(false);
useLayoutEffect(() => {
@@ -270,10 +283,14 @@ export const Layout = ({
)}
- {isDesktop ? (
+ {isDesktop && hasEnabledPanels ? (
{desktopAddonsPanelOpen ? (
- setDesktopAddonsPanelOpen(false)} />
+ setDesktopAddonsPanelOpen(false)}
+ />
) : (
- addonPanelRef.current.setAddonsPanelOpen(true)}
- Icon={BottomBarToggleIcon}
- />
+ {hasEnabledPanels && (
+ addonPanelRef.current.setAddonsPanelOpen(true)}
+ Icon={BottomBarToggleIcon}
+ />
+ )}
) : null}
@@ -330,7 +349,9 @@ export const Layout = ({
) : null}
- {!isDesktop ? : null}
+ {!isDesktop && hasEnabledPanels ? (
+
+ ) : null}
);
};
diff --git a/packages/react-native-ui/src/MobileAddonsPanel.tsx b/packages/react-native-ui/src/MobileAddonsPanel.tsx
index 6a845170b4..ea5efd0f6b 100644
--- a/packages/react-native-ui/src/MobileAddonsPanel.tsx
+++ b/packages/react-native-ui/src/MobileAddonsPanel.tsx
@@ -1,6 +1,7 @@
import { BottomSheetModal } from '@gorhom/bottom-sheet';
import { addons } from 'storybook/manager-api';
import { styled, useTheme } from '@storybook/react-native-theming';
+import type { Parameters } from 'storybook/internal/csf';
import {
Addon_TypesEnum,
type Addon_BaseType,
@@ -31,8 +32,10 @@ const contentStyle = {
flex: 1,
} satisfies StyleProp;
-export const MobileAddonsPanel = forwardRef(
- ({ storyId }, ref) => {
+type MobileAddonsPanelProps = { storyId?: string; parameters?: Parameters };
+
+export const MobileAddonsPanel = forwardRef(
+ ({ storyId, parameters }, ref) => {
const theme = useTheme();
const reducedMotion = useReducedMotion();
@@ -103,6 +106,7 @@ export const MobileAddonsPanel = forwardRef
@@ -148,8 +152,22 @@ const hiddenStyle = {
const hitSlop = { top: 10, right: 10, bottom: 10, left: 10 };
-export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId?: string }) => {
- const panels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL);
+export const AddonsTabs = ({
+ onClose,
+ storyId,
+ parameters,
+}: {
+ onClose?: () => void;
+ storyId?: string;
+ parameters?: Parameters;
+}) => {
+ const panels = useMemo>(() => {
+ const allPanels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL);
+
+ return Object.fromEntries(
+ Object.entries(allPanels).filter(([, p]) => !p.paramKey || !parameters?.[p.paramKey]?.disable)
+ );
+ }, [parameters]);
const [addonSelected, setAddonSelected] = useState(Object.keys(panels)[0]);
@@ -162,6 +180,7 @@ export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId
});
const panelEntries = useMemo(() => Object.entries(panels), [panels]);
+ const activeAddonId = panels[addonSelected] ? addonSelected : panelEntries[0]?.[0];
return (
@@ -177,7 +196,7 @@ export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId
return (
setAddonSelected(id)}
text={String(resolvedTitle)}
/>
@@ -207,7 +226,7 @@ export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId
) : (
panelEntries.map(([id, p]) => (
-
+
))
diff --git a/packages/react-native-ui/src/MobileMenuDrawer.tsx b/packages/react-native-ui/src/MobileMenuDrawer.tsx
index 6c97a8702c..0116ec3ac0 100644
--- a/packages/react-native-ui/src/MobileMenuDrawer.tsx
+++ b/packages/react-native-ui/src/MobileMenuDrawer.tsx
@@ -13,7 +13,7 @@ import {
useMemo,
useRef,
} from 'react';
-import { Keyboard, Platform } from 'react-native';
+import { Keyboard, Platform, type ViewStyle } from 'react-native';
import { useAnimatedStyle, useReducedMotion } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSelectedNode } from './SelectedNodeProvider';
@@ -44,7 +44,7 @@ export const BottomSheetBackdropComponent = (backdropComponentProps: BottomSheet
pressBehavior={'close'}
style={[
backdropComponentProps.style,
- androidTouchEventFix,
+ androidTouchEventFix as unknown as ViewStyle,
{
backgroundColor: 'rgba(0,0,0,0.5)',
paddingTop: Platform.OS === 'android' ? 1 : undefined,