Custom Components (Level 2)
Replace specific UI components with your own implementations while maintaining the SDK's data flow and state management.
Overview
Custom components allow you to:
- Replace individual UI elements (like answer buttons)
- Add animations
- Integrate analytics
- Extend default behavior
The SDK handles state management and data flow, while you control the presentation.
Available Custom Components
Question Screen Components
QuestionAnswerButton
Replace individual answer buttons in Question screens.
import { QuestionAnswerButtonProps } from "@rocapine/react-native-onboarding";
const CustomButton: React.FC<QuestionAnswerButtonProps> = ({
answer,
selected,
onPress,
theme,
index,
isFirst,
isLast,
}) => (
<TouchableOpacity
onPress={onPress}
style={{
height: 96,
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: theme.colors.neutral.lower,
backgroundColor: selected ? theme.colors.primary : "transparent",
}}
>
<Text style={{ fontSize: 24, color: selected ? "#fff" : theme.colors.text.primary }}>
{answer.label}
</Text>
</TouchableOpacity>
);
<OnboardingProvider
customComponents={{
QuestionAnswerButton: CustomButton
}}
/>
Props:
| Prop | Type | Description |
|---|---|---|
answer | { label: string; value: string } | Answer data |
selected | boolean | Whether this answer is selected |
onPress | () => void | Callback when pressed |
theme | Theme | Current theme tokens |
index | number | Index in the list |
isFirst | boolean | True if first item |
isLast | boolean | True if last item |
QuestionAnswersList
Replace the entire answers list for complete control (animations, layout, etc.).
import {
QuestionAnswersListProps,
DefaultQuestionAnswerButton,
} from "@rocapine/react-native-onboarding";
const AnimatedList: React.FC<QuestionAnswersListProps> = ({
answers,
selected,
onAnswerPress,
theme,
multipleAnswer,
}) => {
const animations = useRef(answers.map(() => new Animated.Value(0))).current;
useEffect(() => {
// Staggered entrance animation
Animated.stagger(150,
animations.map(anim =>
Animated.spring(anim, { toValue: 1, useNativeDriver: true })
)
).start();
}, []);
return (
<View style={{ gap: 10 }}>
{answers.map((answer, index) => (
<Animated.View
key={answer.value}
style={{
opacity: animations[index],
transform: [{
translateY: animations[index].interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
}]
}}
>
<DefaultQuestionAnswerButton
answer={answer}
selected={selected[answer.value]}
onPress={() => onAnswerPress(answer.value)}
theme={theme}
index={index}
isFirst={index === 0}
isLast={index === answers.length - 1}
/>
</Animated.View>
))}
</View>
);
};
<OnboardingProvider
customComponents={{
QuestionAnswersList: AnimatedList
}}
/>
Props:
| Prop | Type | Description |
|---|---|---|
answers | Array<{ label: string; value: string }> | All answer options |
selected | Record<string, boolean> | Map of selected answers |
onAnswerPress | (value: string) => void | Callback when answer pressed |
theme | Theme | Current theme tokens |
multipleAnswer | boolean | Allow multiple selections |
Component Priority
Components are resolved in this order:
- Custom List (
QuestionAnswersList) - Takes complete control - Custom Button (
QuestionAnswerButton) - Used by default list - Default Implementation - Built-in SDK styling
// If you provide both:
<OnboardingProvider
customComponents={{
QuestionAnswersList: MyList, // This wins
QuestionAnswerButton: MyButton, // Ignored when MyList is provided
}}
/>
Providing QuestionAnswersList overrides QuestionAnswerButton because the list component has complete control over rendering.
Usage Patterns
Pattern 1: Simple Button Styling
Replace button appearance while keeping default list logic:
import { TouchableOpacity, Text } from "react-native";
import { QuestionAnswerButtonProps } from "@rocapine/react-native-onboarding";
const MinimalButton: React.FC<QuestionAnswerButtonProps> = ({
answer,
selected,
onPress,
theme,
}) => (
<TouchableOpacity
onPress={onPress}
style={{
height: 96,
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: "#e5e5e5",
backgroundColor: selected ? theme.colors.primary : "transparent",
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ fontSize: 24, color: theme.colors.text.primary }}>
{answer.label}
</Text>
</TouchableOpacity>
);
Use when:
- You only need to change button styling
- Default list layout and spacing works for you
- No custom animations needed
Pattern 2: Animated List
Full control over the list with animations:
import React, { useEffect, useRef } from "react";
import { View, Animated } from "react-native";
import {
QuestionAnswersListProps,
DefaultQuestionAnswerButton,
} from "@rocapine/react-native-onboarding";
const AnimatedList: React.FC<QuestionAnswersListProps> = ({
answers,
selected,
onAnswerPress,
theme,
}) => {
const animations = useRef(answers.map(() => new Animated.Value(0))).current;
useEffect(() => {
Animated.stagger(150,
animations.map(anim =>
Animated.spring(anim, {
toValue: 1,
useNativeDriver: true,
tension: 50,
friction: 7,
})
)
).start();
}, []);
return (
<View style={{ gap: 10 }}>
{answers.map((answer, index) => (
<Animated.View
key={answer.value}
style={{
opacity: animations[index],
transform: [{
translateY: animations[index].interpolate({
inputRange: [0, 1],
outputRange: [20, 0],
}),
}],
}}
>
<DefaultQuestionAnswerButton
answer={answer}
selected={selected[answer.value]}
onPress={() => onAnswerPress(answer.value)}
theme={theme}
index={index}
isFirst={index === 0}
isLast={index === answers.length - 1}
/>
</Animated.View>
))}
</View>
);
};
Use when:
- You want coordinated animations across items
- Custom layout logic is needed
- Custom spacing or gaps required
Pattern 3: Wrapping Default Components
Extend default behavior (e.g., analytics):
import { DefaultQuestionAnswerButton } from "@rocapine/react-native-onboarding";
const TrackedButton: React.FC<QuestionAnswerButtonProps> = (props) => {
const handlePress = () => {
// Add analytics
analytics.track('answer_selected', {
value: props.answer.value,
label: props.answer.label,
});
// Call original handler
props.onPress();
};
return <DefaultQuestionAnswerButton {...props} onPress={handlePress} />;
};
Use when:
- You want to add behavior without changing appearance
- Wrapping for logging, analytics, or tracking
- Adding additional side effects
Default Components Reference
Use these to compose or extend the default styling:
DefaultQuestionAnswerButton
import { DefaultQuestionAnswerButton } from "@rocapine/react-native-onboarding";
Features:
- Rounded corners (borderRadius: 16)
- Theme-aware colors
- Semantic text styles (body)
- 2px border
- 20px vertical padding, 24px horizontal
DefaultQuestionAnswersList
import { DefaultQuestionAnswersList } from "@rocapine/react-native-onboarding";
Features:
- 10px gap between items
- Maps over answers
- Uses button component from context or default
Complete Examples
Example 1: Minimal Figma Design
Matching a specific Figma design with top/bottom borders only:
// components/MinimalAnswerButton.tsx
import React from "react";
import { TouchableOpacity, Text, StyleSheet } from "react-native";
import { QuestionAnswerButtonProps } from "@rocapine/react-native-onboarding";
export const MinimalAnswerButton: React.FC<QuestionAnswerButtonProps> = ({
answer,
selected,
onPress,
theme,
}) => (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
style={[
styles.button,
{ borderColor: theme.colors.neutral.lower },
selected && {
backgroundColor: theme.colors.neutral.lowest,
borderLeftWidth: 4,
borderLeftColor: theme.colors.text.primary,
},
]}
>
<Text style={[styles.text, { color: theme.colors.text.primary }]}>
{answer.label}
</Text>
</TouchableOpacity>
);
const styles = StyleSheet.create({
button: {
height: 96,
borderTopWidth: 1,
borderBottomWidth: 1,
borderLeftWidth: 0,
borderRightWidth: 0,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 24,
},
text: {
fontSize: 24,
textAlign: "center",
},
});
// app/_layout.tsx
<OnboardingProvider
customComponents={{
QuestionAnswerButton: MinimalAnswerButton,
}}
/>
Example 2: Staggered Animation
Answers that fade and slide in one after another:
// components/AnimatedAnswersList.tsx
import React, { useEffect, useRef } from "react";
import { View, Animated, StyleSheet } from "react-native";
import {
QuestionAnswersListProps,
DefaultQuestionAnswerButton,
} from "@rocapine/react-native-onboarding";
export const AnimatedAnswersList: React.FC<QuestionAnswersListProps> = ({
answers,
selected,
onAnswerPress,
theme,
}) => {
const animations = useRef(answers.map(() => new Animated.Value(0))).current;
useEffect(() => {
const stagger = Animated.stagger(
150,
animations.map((anim) =>
Animated.spring(anim, {
toValue: 1,
useNativeDriver: true,
tension: 50,
friction: 7,
})
)
);
stagger.start();
}, [animations]);
return (
<View style={styles.container}>
{answers.map((answer, index) => (
<Animated.View
key={answer.value}
style={{
opacity: animations[index],
transform: [{
translateY: animations[index].interpolate({
inputRange: [0, 1],
outputRange: [20, 0],
}),
}],
}}
>
<DefaultQuestionAnswerButton
answer={answer}
selected={selected[answer.value]}
onPress={() => onAnswerPress(answer.value)}
theme={theme}
index={index}
isFirst={index === 0}
isLast={index === answers.length - 1}
/>
</Animated.View>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
gap: 10,
},
});
// app/_layout.tsx
<OnboardingProvider
customComponents={{
QuestionAnswersList: AnimatedAnswersList,
}}
/>
Best Practices
1. Always Use Theme Prop
// ✅ Good
<View style={{ backgroundColor: theme.colors.surface.lowest }} />
// ❌ Bad
<View style={{ backgroundColor: "#FFFFFF" }} />
2. Implement Exact Props Interface
// ✅ Good
const MyButton: React.FC<QuestionAnswerButtonProps> = (props) => { ... }
// ❌ Bad
const MyButton = ({ answer, selected }) => { ... }
3. Use Default Components When Possible
// ✅ Good - Compose with defaults
<DefaultQuestionAnswerButton {...props} />
// ⚠️ Consider - Only if defaults don't work
<CompletelyCustomButton {...props} />
4. Memoize Complex Components
export const AnimatedList = React.memo<QuestionAnswersListProps>(({ ... }) => {
// Complex rendering logic
});
5. Maintain Accessibility
<TouchableOpacity
accessible={true}
accessibilityRole="button"
accessibilityLabel={answer.label}
accessibilityState={{ selected }}
onPress={onPress}
>
{/* ... */}
</TouchableOpacity>
Testing Custom Components
-
Build the SDK:
npm run build -
Test in example app:
cd example
npm start -
Add to provider:
<OnboardingProvider
customComponents={{
QuestionAnswerButton: YourCustomButton,
}}
/> -
Test both states:
- Unselected state
- Selected state
- Single vs. multiple selection
- Theme changes
Future Custom Components
More customizable components coming soon:
- Carousel slide components
- Picker input components
- MediaContent renderers
- Progress indicators
Next Steps
- 🎭 Custom Renderers - Complete screen control (Level 3)
- 🎨 Theming - Combine with theme customization
- 📘 API Reference - Full component prop reference