software-mansion / software-mansion/react-native-screens
iOS 26 header button extends itself intermittently
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.7k
- Forks
- 714
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 71
Description
Description
Using Expo 54 and react-native-screens 4.16.0. On iOS 26, the custom header button on the right, extends itself intermittently. It's hard to reproduce consistently, but happens quite frequently.
headerButton.tsx
import { tokens } from '@/design';
import { color } from '@/design/colors';
import React from 'react';
import { AccessibilityRole, Platform, StyleProp, View, ViewStyle } from 'react-native';
import { BorderlessButton, RectButton } from 'react-native-gesture-handler';
import { Pressable } from './Pressable';
import { isLiquidGlassAvailable } from 'expo-glass-effect';
interface HeaderButtonProps {
onPress?: () => void;
children: React.ReactNode;
testID?: string;
accessibilityRole?: AccessibilityRole;
accessibilityLabel?: string;
disabled?: boolean;
rippleShape?: 'circle' | 'rect';
style?: StyleProp<ViewStyle>;
}
const InnerContent = ({ children }: { children: React.ReactNode }) => (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: tokens.spacing.xs }}>
{children}
</View>
);
export function HeaderButton({
onPress,
children,
testID,
accessibilityRole = 'button',
accessibilityLabel,
disabled = false,
rippleShape = 'circle',
style,
}: HeaderButtonProps) {
if (Platform.OS === 'android') {
if (rippleShape === 'rect') {
return (
<RectButton
onPress={onPress}
testID={testID}
accessibilityRole={accessibilityRole}
accessibilityLabel={accessibilityLabel}
enabled={!disabled}
rippleColor={color.androidHighlight}
style={[
{
paddingHorizontal: 6,
opacity: disabled ? 0.3 : 1,
minWidth: 48,
minHeight: 48,
borderRadius: tokens.radius.full,
justifyContent: 'center',
alignItems: 'center',
},
style,
]}
>
<InnerContent>{children}</InnerContent>
</RectButton>
);
}
return (
<BorderlessButton
onPress={onPress}
testID={testID}
accessibilityRole={accessibilityRole}
accessibilityLabel={accessibilityLabel}
enabled={!disabled}
rippleColor={color.androidHighlight}
hitSlop={16}
rippleRadius={20}
style={[
{
paddingHorizontal: 0,
opacity: disabled ? 0.3 : 1,
justifyContent: 'center',
alignItems: 'center',
},
style,
]}
>
<InnerContent>{children}</InnerContent>
</BorderlessButton>
);
}
return (
<Pressable
onPress={onPress}
testID={testID}
accessibilityRole={accessibilityRole}
accessibilityLabel={accessibilityLabel}
disabled={disabled}
haptic
hitSlop={8}
style={[
{
paddingHorizontal: isLiquidGlassAvailable() ? 6 : undefined,
opacity: disabled ? 0.3 : 1,
justifyContent: 'center',
alignItems: 'center',
},
style,
]}
>
<InnerContent>{children}</InnerContent>
</Pressable>
);
}
HeaderMenuButton.tsx
import { MenuAction, MenuView, NativeActionEvent } from '@react-native-menu/menu';
import { isLiquidGlassAvailable } from 'expo-glass-effect';
import React from 'react';
import { type AccessibilityRole, Platform, StyleSheet, View } from 'react-native';
import { HeaderButton } from './HeaderButton';
// Liquid Glass headers render icon-only actions with a noticeably smaller circular visual than the
// pre-iOS-26 header buttons. Matching that 36pt visual keeps the menu trigger aligned with the
// existing checkmark/save buttons while still constraining the native MenuView anchor.
const LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE = 36;
type AccessibleMenuViewProps = React.ComponentProps<typeof MenuView> & {
accessibilityLabel?: string;
accessibilityRole?: AccessibilityRole;
};
// TODO: Remove this once @react-native-menu/menu exposes standard accessibility props in its TS types.
const AccessibleMenuView = MenuView as React.ComponentType<AccessibleMenuViewProps>;
interface HeaderMenuButtonProps {
actions: MenuAction[];
onPressAction: ({ nativeEvent }: NativeActionEvent) => void;
children: React.ReactNode;
testID?: string;
accessibilityLabel: string;
}
export function HeaderMenuButton({
actions,
onPressAction,
children,
testID,
accessibilityLabel,
}: HeaderMenuButtonProps) {
const shouldUseConstrainedAnchor = Platform.OS === 'ios' && isLiquidGlassAvailable();
return (
<AccessibleMenuView
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
testID={testID}
actions={actions}
onPressAction={onPressAction}
shouldOpenOnLongPress={false}
style={shouldUseConstrainedAnchor ? styles.menuAnchor : undefined}
>
<View
pointerEvents="none"
accessible={false}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
style={shouldUseConstrainedAnchor ? styles.content : undefined}
>
<HeaderButton
accessibilityLabel={accessibilityLabel}
style={shouldUseConstrainedAnchor ? styles.visualButton : undefined}
>
{children}
</HeaderButton>
</View>
</AccessibleMenuView>
);
}
const styles = StyleSheet.create({
menuAnchor: {
width: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
height: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
minWidth: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
minHeight: LIQUID_GLASS_HEADER_MENU_BUTTON_SIZE,
flexGrow: 0,
flexShrink: 0,
justifyContent: 'center',
alignItems: 'center',
},
content: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
visualButton: {
width: '100%',
height: '100%',
paddingHorizontal: 0,
},
});
Steps to reproduce
Keep viewing from one screen to another until I see the issue. It's really intermittent.
Snack or a link to a repository
N/A
Screens version
4.16.0
React Native version
0.81.5
Platforms
iOS
JavaScript runtime
None
Workflow
None
Architecture
None
Build type
Release mode
Device
Real device
Device model
No response
Acknowledgements
Yes
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the supplied HeaderButton.tsx and HeaderMenuButton.tsx examples, then reproduce repeated navigation on a real iOS 26 device using Expo 54, react-native-screens 4.16.0, and React Native 0.81.5 in release mode. Compare the header button layout across transitions and verify that the right-side custom button no longer expands intermittently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 45/100