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