| name | dev.cds-mobile |
| description | USE THIS when asked to work on a new or existing (MOBILE) CDS React component in packages/mobile |
CDS Mobile Package Guidelines
Mobile-specific patterns for @coinbase/cds-mobile.
Component Config Adoption (Mobile)
Use this guidance when adding ComponentConfigProvider defaults for the specific component you are editing.
Required implementation pattern
- Register the component in
packages/mobile/src/core/componentConfig.ts using its *BaseProps:
import type { MyComponentBaseProps } from '../category/MyComponent';
export type ComponentConfig = {
MyComponent?: ConfigResolver<MyComponentBaseProps>;
};
- Adopt
useComponentConfig in the component and destructure from merged props:
import { useComponentConfig } from '../hooks/useComponentConfig';
export const MyComponent = memo((_props: MyComponentProps) => {
const mergedProps = useComponentConfig('MyComponent', _props);
const { style, ...props } = mergedProps;
return <Pressable style={style} {...props} />;
});
Rules to preserve behavior
- Provider config supplies defaults only; local props must continue to win.
- Use
_props as the input variable and mergedProps as the configured output.
- Type resolver entries with
*BaseProps (not full *Props).
- Keep scope to prop-level theming defaults; do not alter component behavior or control flow.
- When practical during the same change, prefer arrow-function component declarations.
Styling with StyleSheet
Use StyleSheet.create for static styles and useTheme() for dynamic values:
import { StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
const styles = StyleSheet.create({
container: { position: 'relative', width: '100%' },
});
const theme = useTheme();
const dynamicStyle = {
backgroundColor: theme.color.bgPrimary,
padding: theme.space[2],
};
<View style={[styles.container, dynamicStyle, style]} />;
Inline styles
- Mobile components should all expose a
style and styles object props for overriding default styles.
- As styling is a concern of that specific component, the
style and styles props should never be on the *BaseProps type.
styles can be used for granular overrides on child elements within the component.
- Always merge styles into a react-native style array with
useMemo in the correct order (default styles => style prop => styles[ELEMENT_NAME] prop).
Example:
type ComponentProps = ComponentBaseProps & {
style?: StyleProp<ViewStyle>;
styles?: {
root?: StyleProp<ViewStyle>;
label?: StyleProp<TextStyle>;
};
};
const theme = useTheme();
const containerStyles = useMemo(
() => [
{ backgroundColor: theme.color.bgPrimary },
style,
styles.root,
],
[theme.color.bgPrimary, animatedStyles, style]
);
<Box style={containerStyles}>
Animation
React Native Reanimated
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
const opacity = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
transform: [{ translateY: withTiming(opacity.value * -8) }],
}));
<Animated.View style={animatedStyle} />;
We DO NOT use React-Spring anymore for animations on mobile.
Gesture Handling
Use react-native-gesture-handler:
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
const panGesture = useMemo(
() =>
Gesture.Pan()
.onStart(() => {
})
.onUpdate(({ translationX }) => {
})
.onEnd(({ translationX, velocityX }) => {
})
.withTestId(testID)
.runOnJS(true),
[dependencies],
);
<GestureDetector gesture={panGesture}>
<Animated.View>{/* ... */}</Animated.View>
</GestureDetector>;
Layout Measurement
Use onLayout callback instead of ResizeObserver:
const [size, onLayout] = useLayout();
<View onLayout={onLayout} />
<View onLayout={(e) => setHeight(e.nativeEvent.layout.height)} />
Accessibility
- Use appropriate accessibilityLabel, accessibilityHint, and accessibilityRole, accessibilityState props
- Support screen readers (VoiceOver and TalkBack)
- Ensure touch targets meet minimum size requirements (44x44 points)
Example: Use React Native accessibility props:
<View
accessible
accessibilityRole="adjustable"
accessibilityLabel="Product carousel"
accessibilityLiveRegion="polite"
>
<Pressable
accessibilityState={{ selected: isActive, disabled }}
accessibilityActions={[{ name: 'activate' }]}
onAccessibilityAction={handleAccessibilityAction}
/>
</View>
Screen Reader Content
<View accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
{}
</View>
<Text
importantForAccessibility="yes"
accessibilityLiveRegion="polite"
style={{ color: 'transparent', position: 'absolute' }}
>
{accessibleLabel}
</Text>
Reference Components
- SlideButton: gesture handling, spring animations, accessibility actions
- RollingNumber: Reanimated, measurement patterns, screen reader content
- Select (alpha/): controlled/uncontrolled, Drawer integration
- Stepper: direction-based defaults, shared logic from cds-common
- Tour: animations, complexity
- DatePicker: complexity