Skip to main content

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:

PropTypeDescription
answer{ label: string; value: string }Answer data
selectedbooleanWhether this answer is selected
onPress() => voidCallback when pressed
themeThemeCurrent theme tokens
indexnumberIndex in the list
isFirstbooleanTrue if first item
isLastbooleanTrue 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:

PropTypeDescription
answersArray<{ label: string; value: string }>All answer options
selectedRecord<string, boolean>Map of selected answers
onAnswerPress(value: string) => voidCallback when answer pressed
themeThemeCurrent theme tokens
multipleAnswerbooleanAllow multiple selections

Component Priority​

Components are resolved in this order:

  1. Custom List (QuestionAnswersList) - Takes complete control
  2. Custom Button (QuestionAnswerButton) - Used by default list
  3. Default Implementation - Built-in SDK styling
// If you provide both:
<OnboardingProvider
customComponents={{
QuestionAnswersList: MyList, // This wins
QuestionAnswerButton: MyButton, // Ignored when MyList is provided
}}
/>
info

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​

  1. Build the SDK:

    npm run build
  2. Test in example app:

    cd example
    npm start
  3. Add to provider:

    <OnboardingProvider
    customComponents={{
    QuestionAnswerButton: YourCustomButton,
    }}
    />
  4. 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​