Page Types
The SDK includes pre-built renderers for common onboarding patterns.
Overview
Each page type is designed for a specific onboarding use case. All types share common properties and are customizable through theming, custom components, or custom renderers.
Available Page Types
Question
Interactive questions with single or multiple choice answers.
Use cases:
- User preferences
- Quiz-style onboarding
- Survey questions
- Profile building
Key features:
- Single or multiple selection
- Customizable answer buttons
- Theme-aware styling
- Support for custom components
Payload:
{
title: string;
subtitle?: string;
answers: Array<{
label: string;
value: string;
}>;
multipleAnswer: boolean;
}
Example:
{
id: "preferences",
type: "Question",
payload: {
title: "What are your goals?",
subtitle: "Select all that apply",
answers: [
{ label: "Build muscle", value: "muscle" },
{ label: "Lose weight", value: "weight" },
{ label: "Improve flexibility", value: "flexibility" },
],
multipleAnswer: true
}
}
Dependencies: None
MediaContent
Display images or videos with title and description.
Use cases:
- Welcome screens
- Feature explanations
- Visual onboarding
- Tutorial steps
Key features:
- Video, Image, Lottie, or Rive support
- Optional title and description
- Centered media display
- Responsive sizing
Payload:
{
title?: string;
description?: string;
media: {
type: "video" | "image" | "lottie" | "rive";
url?: string;
localPathId?: string;
};
}
Example:
{
id: "welcome",
type: "MediaContent",
payload: {
title: "Welcome to FitApp",
description: "Your personal fitness companion",
media: {
type: "image",
url: "https://example.com/welcome.png"
}
}
}
Dependencies: None
Carousel
Multi-slide horizontal pagination with page indicators.
Use cases:
- Feature tours
- Multi-step tutorials
- Swipeable content
- Progressive disclosure
Key features:
- Horizontal scrolling
- Page indicators (dots)
- Per-slide media and text
- Automatic "Next" / "Continue" button labels
Payload:
{
screens: Array<{
title?: string;
description?: string;
media?: MediaSource;
}>;
pagination?: {
show?: boolean; // default true
dotColor?: string; // default theme.colors.neutral.lower
activeDotColor?: string; // default theme.colors.primary
dotWidth?: number; // default 8
dotHeight?: number; // default 8
activeDotWidth?: number; // default 24
activeDotHeight?: number; // default 8
gap?: number; // default 8
position?: "top" | "bottom"; // default "bottom" — dots above or below content
marginTop?: number; // default 20
marginBottom?: number; // default 20
};
}
The pagination object is optional — omit it to keep the default dot styling. All
sub-fields are optional and individually defaulted, so you can override just the
ones you need (e.g. only activeDotColor and position).
Example:
{
id: "tour",
type: "Carousel",
payload: {
screens: [
{
title: "Track Your Progress",
description: "Monitor your fitness journey",
media: { type: "image", url: "https://..." }
},
{
title: "Set Goals",
description: "Define and achieve your targets",
media: { type: "image", url: "https://..." }
}
]
}
}
Dependencies: None
Picker
Type-specific input pickers for structured data.
Use cases:
- Collect specific user data
- Profile information
- Preferences with constrained options
Key features:
- Native picker UI
- Type-specific implementations
- Unit conversions (weight, height)
- Range validation
Supported types:
weight- Weight with kg/lb unitsheight- Height with cm/ft+in unitsage- Age pickergender- Gender selectionname- Name inputdate- Date picker
Payload:
{
title: string;
subtitle?: string;
pickerType: "weight" | "height" | "age" | "gender" | "name" | "date";
defaultValue?: string;
}
Example:
{
id: "user-weight",
type: "Picker",
payload: {
title: "What's your weight?",
pickerType: "weight",
defaultValue: "70-kg"
}
}
Dependencies: @react-native-picker/picker
npx expo install @react-native-picker/picker
Loader
Sequential progress animation with optional carousel.
Use cases:
- Loading states
- Processing animations
- Wait screens with progress
- Educational "Did you know?" moments
Key features:
- Sequential step animations
- Progress bars (0% → 100%)
- Checkmarks on completion
- Optional carousel with images
- Configurable duration
Payload:
{
title: string;
steps: Array<{
label: string; // Text while loading
completed: string; // Text when done
}>;
didYouKnowImages?: Array<MediaSource>;
duration?: number; // Milliseconds per step (default: 2000)
}
Example:
{
id: "analyzing",
type: "Loader",
payload: {
title: "Analyzing your profile",
steps: [
{
label: "Processing data",
completed: "Data processed"
},
{
label: "Building recommendations",
completed: "Recommendations ready"
}
],
duration: 2000
}
}
Dependencies: None
Ratings
App store rating prompts with social proof.
Use cases:
- Request app reviews
- Show user testimonials
- Build trust with social proof
- Increase app ratings
Key features:
- Star rating UI
- Opens native App Store review
- Social proof cards (optional)
- Customizable testimonials
Payload:
{
title: string;
subtitle?: string;
socialProofs?: Array<{
name: string;
rating: number;
comment: string;
avatar?: string;
}>;
}
Example:
{
id: "rate-us",
type: "Ratings",
payload: {
title: "Enjoying FitApp?",
subtitle: "Your feedback helps us improve!",
socialProofs: [
{
name: "Sarah M.",
rating: 5,
comment: "This app changed my life!",
}
]
}
}
Dependencies: expo-store-review
npx expo install expo-store-review
Commitment
User commitment and agreement screens.
Use cases:
- Terms acceptance
- Commitment statements
- Agreements
- Signature collection
Key features:
- Checkbox for agreement
- Optional signature (requires Skia)
- Info boxes for important notes
- Required acknowledgment before continuing
Payload:
{
title: string;
subtitle?: string;
commitmentText: string;
requireSignature?: boolean;
infoBoxes?: Array<{
title: string;
description: string;
icon?: string;
}>;
}
Example:
{
id: "terms",
type: "Commitment",
payload: {
title: "Terms of Service",
commitmentText: "I agree to the Terms of Service and Privacy Policy",
requireSignature: false,
infoBoxes: [
{
title: "Privacy First",
description: "We never share your data without permission"
}
]
}
}
Dependencies (signature only): @shopify/react-native-skia
npx expo install @shopify/react-native-skia
ComposableScreen
Build arbitrary onboarding screens entirely from CMS data — no custom renderer required. You define a tree of layout and media elements that is rendered directly to native components.
Use cases:
- Fully custom layouts without shipping app updates
- Cards, feature highlights, or rich text screens
- Animation-heavy screens (Lottie or Rive)
- Any screen that doesn't fit a pre-built type
Key features:
- Recursive element tree (
YStack,XStack,ZStack,SafeAreaView,Text,RichText,Image,Lottie,Rive,Icon,Video,Input,RadioGroup,CheckboxGroup,Button,DatePicker,WheelPicker,DrawingPad,Slider,ProgressIndicator,AnimatedText,Carousel) - Full flexbox layout support
- Border, radius, overflow, and opacity control
- Dimension constraints (
width,height,min/max) - Spacing with both
paddingandmargin - Text color defaults to
theme.colors.text.primary - Lottie, Rive, Video, DatePicker, WheelPicker, Slider, and DrawingPad as optional peer deps — graceful fallback if not installed (DrawingPad throws an explicit install error)
- Variable context —
Input,RadioGroup,CheckboxGroup,DatePicker,WheelPicker,Slider, andDrawingPadelements write into shared state;Textelements interpolate those values with{{variableName}}expressions - Conditional rendering — every UIElement accepts
renderWhen?: LeafCondition | ConditionGroup; the renderer skips the element and its subtree when the condition is false
Element types:
| Element | Renders as | Direction | Peer dep |
|---|---|---|---|
YStack | <View> | column | — |
XStack | <View> | row | — |
ZStack | <View> | depth (z-axis) | — |
SafeAreaView | <SafeAreaView> | column | react-native-safe-area-context |
Text | <Text> | — | — |
RichText | <View> wrapping Text children | wrapping row | — |
Image | <Image> | — | — |
ProgressiveBlurImage | <Image> + masked blurred image copy | depth (z-axis) | @react-native-masked-view/masked-view, expo-linear-gradient, expo-image (all optional) |
Icon | Lucide icon | — | — (bundled) |
Input | <TextInput> | — | — |
RadioGroup | Radio item list | — | — |
CheckboxGroup | Checkbox item list | — | — |
Button | <TouchableOpacity> | — | — |
Carousel | Horizontal slides | — | react-native-reanimated-carousel |
DatePicker | Native date/time picker | — | @react-native-community/datetimepicker |
WheelPicker | Native scrolling wheel selector | — | @react-native-picker/picker |
DrawingPad | Freehand draw/signature canvas | — | @shopify/react-native-skia |
Slider | Continuous numeric slider | — | @react-native-community/slider |
ProgressIndicator | Linear bar / circular ring | — | — |
AnimatedText | Count-up number (UI-thread, native TextInput) | — | — |
Lottie | LottieView | — | lottie-react-native |
Rive | <Rive> | — | rive-react-native |
Video | <VideoView> | — | expo-video |
Animations, transitions & effects (every element)
Every UIElement inherits an optional transform and animation from BaseBoxProps, so motion can be attached to any element type. The schema mirrors react-native-reanimated — preset values are the exact reanimated builder names and the modifier fields map to builder methods (.duration().delay().springify().easing()). react-native-reanimated >=3 is a peer dependency (already required for Carousel / ProgressIndicator); no extra install.
transform — static, also the surface effect animates:
| Field | Type | Notes |
|---|---|---|
translateX / translateY | number | px offset |
scale / scaleX / scaleY | number | |
rotate | number | degrees |
animation — { entering?, exiting?, layout?, effect? }:
| Sub-field | Shape | Notes |
|---|---|---|
entering | { preset, duration?, delay?, easing?, spring? } | Plays once on mount |
exiting | { preset, duration?, delay?, easing?, spring? } | Plays on unmount (e.g. when renderWhen flips false) |
layout | { preset, duration?, spring? } | Animates re-layout when siblings appear/disappear |
effect | { preset, duration?, delay?, easing?, loop?, … } | Continuous loop (withRepeat) |
easing:"linear" \| "ease-in" \| "ease-out" \| "ease-in-out".spring({ damping?, stiffness?, mass? }) mirrors.springify(config)and wins overeasingwhen both are set.- Entering presets (reanimated builder names):
FadeIn(Up/Down/Left/Right),SlideIn(Up/Down/Left/Right),ZoomIn(Rotate/Up/Down/Left/Right/EasyUp/EasyDown),BounceIn(Up/Down/Left/Right),FlipIn(XUp/YLeft/XDown/YRight/EasyX/EasyY),StretchIn(X/Y),RotateIn(DownLeft/DownRight/UpLeft/UpRight),RollIn(Left/Right),PinwheelIn,LightSpeedIn(Left/Right). - Exiting presets: the
…Out…counterparts of the above. - Layout presets:
LinearTransition,FadingTransition,SequencedTransition,JumpingTransition,CurvedTransition,EntryExitTransition. - Effect presets (looping, not reanimated-named):
pulse(minScale/maxScale),fade(minOpacity),rotate(degrees),shimmer,bounce.loop: falseruns the effect once. - Unknown presets degrade to no-op (forward-compatible across reanimated versions). On web some entering presets degrade gracefully (content still mounts).
{
"id": "hero-badge",
"type": "Image",
"props": {
"url": "https://cdn.example.com/badge.png",
"width": 96,
"transform": { "rotate": -4 },
"animation": {
"entering": { "preset": "ZoomInDown", "duration": 600, "delay": 200, "spring": { "damping": 12, "stiffness": 180 } },
"exiting": { "preset": "FadeOut", "duration": 250 },
"effect": { "preset": "pulse", "duration": 1200, "minScale": 0.97, "maxScale": 1.06 }
}
}
}
onPress — make any element tappable (every element)
Every UIElement also inherits an optional onPress: ButtonAction[] from BaseBoxProps — the same action list as Button.actions ("continue", { type: "setVariable", … }, { type: "custom", … }, run sequentially, "continue" terminal; see the Button actions section below). Use it to make a static element react to a tap — a tappable card/Image, a logo that advances the flow, a Text row that fires a custom action.
{
"id": "plan-card",
"type": "YStack",
"props": {
"padding": 16,
"borderRadius": 12,
"onPress": [{ "type": "setVariable", "name": "plan", "value": "pro" }, "continue"]
},
"children": []
}
Runtime ignores onPress on elements that already own a tap / focus / scroll / drag gesture: Button (use actions), RadioGroup, CheckboxGroup, DatePicker, Input, WheelPicker, Slider. The schema still accepts it everywhere (it lives on BaseBoxProps), so wire selection / continue through those elements' own props instead.
Stack props (YStack / XStack):
| Prop | Type | Notes |
|---|---|---|
gap | number | Space between children |
padding / paddingHorizontal / paddingVertical | number | |
margin / marginHorizontal / marginVertical | number | |
flex / flexShrink / flexWrap | number | string | flexShrink defaults to 1 inside XStack |
alignItems | "flex-start" | "center" | "flex-end" | "stretch" | |
alignSelf | "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline" | |
justifyContent | "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | |
width / height / minWidth / maxWidth / minHeight / maxHeight | number | |
backgroundColor / borderColor | string | |
borderWidth / borderRadius | number | |
overflow | "hidden" | "visible" | "scroll" | Use "hidden" to clip rounded corners |
opacity | number |
ZStack props:
ZStack accepts only the BaseBoxProps subset listed below. Flex/flow props such as gap, alignItems, justifyContent, and flexWrap are not honored — children are absolutely positioned, so flex layout does not apply.
| Prop | Type | Notes |
|---|---|---|
width / height / minWidth / maxWidth / minHeight / maxHeight | number | Explicit height is recommended — absolute children do not size the parent |
padding / paddingHorizontal / paddingVertical | number | Applied to the ZStack container |
margin / marginHorizontal / marginVertical | number | |
flex / flexShrink / flexGrow | number | Apply to the ZStack itself within a parent flex container |
alignSelf | "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline" | |
backgroundColor / backgroundGradient | string / GradientBackground | |
borderWidth / borderRadius / borderColor | number | string | |
overflow | "hidden" | "visible" | "scroll" | Use "hidden" to clip overflowing children to rounded corners |
opacity | number |
Children are layered in declaration order: first child = bottom, last child = top. Each child is wrapped in position: "absolute" filling the container, so flex props on ZStack itself (gap, alignItems, justifyContent) have no effect — instead control the layout of each layer's content via the layer's own props (e.g. wrap content in a YStack with justifyContent: "flex-end" to anchor it to the bottom).
The wrapper for each child uses pointerEvents="box-none", so touches pass through transparent regions to layers underneath.
SafeAreaView props:
SafeAreaView mirrors the props of SafeAreaView from react-native-safe-area-context. Use it to inset content from system UI (notch, home indicator) inside a ComposableScreen. Other renderers wrap themselves in SafeAreaView automatically; ComposableScreen does not, so opt in here when needed.
| Prop | Type | Notes |
|---|---|---|
mode | "padding" | "margin" | Defaults to "padding". "margin" is useful when the SafeAreaView itself has a background that should not extend into the inset region |
edges | Edge[] or { top?, right?, bottom?, left? } | Edge is "top" | "right" | "bottom" | "left". Object form maps each edge to "off" | "additive" | "maximum" for fine-grained control |
All BaseBoxProps | — | flex, padding, backgroundColor, borderRadius, etc. |
SafeAreaView example:
{
"id": "safe-root",
"type": "SafeAreaView",
"props": { "flex": 1, "edges": ["top", "bottom"] },
"children": [
{ "id": "content", "type": "YStack", "props": { "padding": 24, "gap": 16 }, "children": [ /* ... */ ] }
]
}
As of 1.15.0, OnboardingTemplate does not apply safe-area insets. Built-in page types (Question, MediaContent, Carousel, etc.) now wrap themselves with SafeAreaView edges={["top","bottom"]}. For ComposableScreen, place a SafeAreaView UIElement at the root of your tree (or omit it for full edge-to-edge rendering).
Image props:
| Prop | Type | Notes |
|---|---|---|
url | string | Required — remote image URI |
width | number | Defaults to "100%" |
height | number | When omitted, aspectRatio is used to size the image |
aspectRatio | number | Used when height is absent; defaults to 16/9 |
resizeMode | "cover" | "contain" | "stretch" | "center" | Defaults to "cover". For SVG sources it maps to preserveAspectRatio |
blurRadius | number | Uniform Gaussian blur radius in px (native, no extra dep). 0/omitted = sharp. Ignored for SVGs |
borderRadius / borderWidth | number | |
borderColor | string | |
opacity | number | |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number |
- WebP / AVIF: when the optional peer dep
expo-imageis installed,Imagerenders through it for reliable WebP/AVIF decoding (React Native's built-inImageis unreliable for WebP on iOS). Withoutexpo-image, it falls back to React Native'sImage. - SVG: a
urlwhose path ends in.svg(query string / hash tolerant) is auto-detected and rendered withreact-native-svg'sSvgUri— no schema change, existing.svgURLs just work.resizeModemaps to the SVGpreserveAspectRatio.
ProgressiveBlurImage props:
A full-bleed image with a gradient-masked Gaussian blur baked in: sharp where the mask is transparent, progressively blurred where it's opaque (the Figma "welcome screen" hero look — sharp subject up top, blurred toward the bottom so overlaid white text stays legible). Self-contained — drop it as the bottom layer of a ZStack with sharp foreground content above it.
| Prop | Type | Notes |
|---|---|---|
url | string | Required — remote image URI |
intensity | number | Required — blur strength, 0–100 (mapped to the blurred copy's blur radius) |
mask | linear { from, to, stops } or radial { type: "radial", center?, radius?, stops } | Required — gradient driving where the blur is strong. Each stop's opacity (0–1) is the blur strength at position (0–1), not a color. Linear: from/to are edges ("top", "bottom", …), type optional. Radial: center {x,y} in 0–1 box fractions (default {0.5,0.5}), radius a 0–1 fraction (default 0.75); position runs center→edge |
tint | "light" | "dark" | "default" | Blur tint. Defaults to "default" |
maxBlurOpacity | number | Clamp on the masked blur layer (0–1). Defaults to 1 |
blurAppear | { delay?, duration?, easing? } | Optional. Fades the blur layer in over the always-visible sharp image: delay (ms, default 0), duration (ms, default 400), easing ("linear" | "ease-in" | "ease-out" | "ease-in-out", default "ease-out"). Omit → blur shows statically at full strength on mount |
resizeMode | "cover" | "contain" | "stretch" | "center" | Defaults to "cover" |
aspectRatio / width / height / borderRadius / opacity / margin* | Standard box props |
The progressive blur masks a blurred copy of the image (not a backdrop BlurView — a masked BlurView has no backdrop to sample and renders transparent on iOS). It needs @react-native-masked-view/masked-view + expo-image (for the blurred copy), all optional. The linear mask renders via expo-linear-gradient (optional); the radial mask renders via react-native-svg (a required dep, always available). When the masked-view native module is missing (incl. a dev-client built before it was installed), the element degrades to a sharp image + a dark gradient scrim derived from the mask (still legible for overlaid text) and never throws.
Text props:
| Prop | Type | Notes |
|---|---|---|
content | string | TextSpan[] | Required. A string, or an array of styled spans for inline rich text (see below) |
mode | "plain" | "expression" | "plain" (default) renders content as-is; "expression" interpolates {{variableName}} patterns from the variable context (in each span's text when content is a span array) |
fontSize / fontWeight / letterSpacing / lineHeight | number | string | |
fontFamily | string | Font family name; must be loaded by the host app (e.g. via expo-font) |
color | string | Defaults to theme.colors.text.primary |
textAlign | "left" | "center" | "right" | |
backgroundColor / borderColor | string | |
padding / paddingHorizontal / paddingVertical | number | |
margin / marginHorizontal / marginVertical | number | |
borderWidth / borderRadius / opacity | number |
Inline rich text (TextSpan):
Set content to an array of spans to style fragments differently within a single line — they render as nested <Text> and wrap together on one baseline. Each span inherits any prop it omits from the parent Text, so a span only declares what it overrides.
{
"type": "Text",
"props": {
"content": [
{ "text": "Lose " },
{ "text": "5kg", "fontWeight": "700", "color": "#E11D48" },
{ "text": " in 30 days — " },
{ "text": "guaranteed", "fontStyle": "italic", "textDecorationLine": "underline" }
],
"fontSize": 16,
"textAlign": "center"
}
}
| Span prop | Type | Notes |
|---|---|---|
text | string | Required. The span's text (interpolated in mode: "expression") |
fontWeight | string | |
fontStyle | "normal" | "italic" | |
fontFamily | string | "inherit" | Defaults to the parent Text's resolved family |
fontSize / letterSpacing | number | |
color | string | |
textDecorationLine | "none" | "underline" | "line-through" | "underline line-through" |
RichText props:
RichText is a wrapping flex row of child Text elements — words and styled "chips" (padded, rounded, rotated) that wrap to the next line and align together. Because each child is a real flex child of a <View> (not a nested <Text>), it honors its own box props (padding, borderRadius, borderWidth, backgroundColor, margin, transform), plus renderWhen and expression mode — none of which inline TextSpans support. Reach for inline spans (Text.content[]) instead when you only need plain character-level styled runs on one baseline.
Plain-text children are automatically split into one item per word so the row wraps word-by-word like a paragraph (a chip then flows inline with naturally-wrapping text). Children that carry box styling (backgroundColor / borderRadius / border / padding) or motion stay atomic — they are the "chips". Because the split preserves spaces as real items, don't set gap when mixing words and chips (it would double-space) — give chips marginHorizontal for spacing instead.
childrenare restricted toTextelements only (a non-Textchild fails schema parse).propsare layout props + allBaseBoxProps+ inherited text-style defaults. The text-style fields (fontSize,fontWeight,fontFamily,fontStyle,color,textAlign,letterSpacing,lineHeight) are handed down to every childText— declare the title's base typography once on the container and only override per-chip on the children (a child prop always wins over the inherited value).
| Prop | Type | Notes |
|---|---|---|
flexWrap | "wrap" | "nowrap" | Defaults to "wrap" |
gap | number | Space between children |
alignItems | "flex-start" | "center" | "flex-end" | "baseline" | "stretch" | "baseline" aligns text baselines across differently-sized segments |
justifyContent | "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | |
fontSize / fontWeight / fontFamily / fontStyle / color / letterSpacing / lineHeight | same as Text | Inherited default for every child Text (child overrides win) |
textAlign | "left" | "center" | "right" | Inherited default for every child Text, and aligns the wrapping row itself: maps to the row's justifyContent (left→flex-start, center→center, right→flex-end) when justifyContent is not set explicitly |
{
"type": "RichText",
"props": { "alignItems": "center", "justifyContent": "center", "fontSize": 22, "fontWeight": "600" },
"children": [
{ "id": "rt-1", "type": "Text", "props": { "content": "Boost your" } },
{
"id": "rt-2",
"type": "Text",
"props": {
"content": "energy", "fontWeight": "700", "color": "#FFFFFF",
"backgroundColor": "#E11D48", "paddingHorizontal": 14, "paddingVertical": 4,
"borderRadius": 200, "marginHorizontal": 4, "transform": { "rotate": -3 }
}
},
{
"id": "rt-3",
"type": "Text",
"renderWhen": { "variable": "name", "operator": "is_not_empty" },
"props": { "content": ", {{name}}!", "mode": "expression" }
}
]
}
Input props:
| Prop | Type | Notes |
|---|---|---|
variableName | string | Context key — value is written to shared composableVariables on every keystroke |
placeholder | string | Placeholder text |
placeholderColor | string | Defaults to theme.colors.text.tertiary |
defaultValue | string | Initial value; also written to context on mount if variableName is set |
keyboardType | "default" | "email-address" | "numeric" | "phone-pad" | "decimal-pad" | "url" | "number-pad" | "ascii-capable" | "numbers-and-punctuation" | "name-phone-pad" | "twitter" | "web-search" | "visible-password" | Defaults to "default" |
returnKeyType | "done" | "go" | "next" | "search" | "send" | "default" | "emergency-call" | "google" | "join" | "route" | "yahoo" | "none" | "previous" | Defaults to "done" |
autoCapitalize | "none" | "sentences" | "words" | "characters" | Defaults to "sentences" |
secureTextEntry | boolean | Mask input for passwords; defaults to false |
maxLength | number | Maximum character count |
multiline | boolean | Enable multi-line input; defaults to false |
numberOfLines | number | Visible line count when multiline is true |
editable | boolean | Defaults to true |
autoFocus | boolean | Focus on mount and open the keyboard; defaults to false |
color | string | Text color; defaults to theme.colors.text.primary |
fontSize | number | Defaults to 16 |
textAlign | "left" | "center" | "right" | |
padding / paddingHorizontal / paddingVertical | number | Inner padding; defaults to 12 |
backgroundColor | string | Defaults to theme.colors.neutral.lowest |
borderWidth | number | Defaults to 1 |
borderRadius | number | Defaults to 8 |
borderColor | string | Defaults to theme.colors.neutral.low |
alignSelf | "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline" | |
width / height | number | |
opacity | number | |
margin / marginHorizontal / marginVertical | number |
RadioGroup props:
| Prop | Type | Notes |
|---|---|---|
items | Array<{ label?: string; value: string; subLabel?: string }> | Required — list of options. label is optional (omit for a sub-label-only row); value is required; subLabel is an optional secondary line under the label |
variableName | string | Context key — selected entry (value + label) is written to shared composableVariables |
defaultValue | string | Pre-selects the matching item on mount; also written to context |
direction | "vertical" | "horizontal" | Layout direction; defaults to "vertical" |
gap | number | Space between items; defaults to 8 |
itemBackgroundColor | string | Unselected item background; defaults to "transparent" |
itemSelectedBackgroundColor | string | Selected item background; defaults to primary + 15% opacity |
itemBorderColor | string | Unselected item border; defaults to theme.colors.neutral.low |
itemSelectedBorderColor | string | Selected item border; defaults to theme.colors.primary |
itemBorderRadius | number | Defaults to 8 |
itemBorderWidth | number | Defaults to 1 |
itemColor | string | Unselected label color; defaults to theme.colors.text.primary |
itemSelectedColor | string | Selected label color; defaults to theme.colors.primary |
showTick | boolean | Show/hide the radio circle indicator. When false, the label and selected background/border still render. Defaults to true |
tickPosition | "start" | "end" | Tick placement relative to the label; defaults to "start" |
tickColor | string | Tick color (border + dot) when unselected; defaults to theme.colors.neutral.medium |
tickSelectedColor | string | Tick color (border + dot) when selected; defaults to theme.colors.primary |
tickBorderRadius | number | Corner radius of the tick element; defaults to tickSize / 2 (full circle) |
tickSize | number | Diameter of the tick circle in px; the inner dot scales with it; defaults to 20 |
haptic | "none" | "light" | "medium" | "heavy" | "soft" | "rigid" | Tactile feedback fired on selection. Maps to expo-haptics ImpactFeedbackStyle. Opt-in — omit or "none" for no feedback. Requires the optional expo-haptics peer dep (no-op if not installed) |
itemFontSize | number | |
itemFontWeight | string | |
itemFontFamily | string | |
itemSubLabelColor | string | Sub-label color when unselected; defaults to theme.colors.text.secondary |
itemSelectedSubLabelColor | string | Sub-label color when selected; falls back to itemSubLabelColor then the selected label color |
itemSubLabelFontSize | number | Defaults to the caption text-style size |
itemSubLabelFontWeight | string | |
itemSubLabelFontFamily | string | Falls back to itemFontFamily, then the theme font |
itemSubLabelFontStyle | "normal" | "italic" | |
itemPadding | number | Defaults to 12 when no itemPaddingHorizontal/Vertical is set |
itemPaddingHorizontal / itemPaddingVertical | number | |
itemShadowColor | string | Per-item iOS drop shadow color. Setting only this defaults itemShadowOpacity to 1, itemShadowRadius to 4 |
itemShadowOffset | { width: number; height: number } | Per-item shadow offset |
itemShadowOpacity | number | 0–1 |
itemShadowRadius | number | Shadow blur radius |
itemElevation | number | Per-item Android elevation shadow |
alignSelf | "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline" | |
width / height | number | Container dimensions |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth / borderRadius / borderColor | number | string | Container border |
opacity | number |
CheckboxGroup props:
| Prop | Type | Notes |
|---|---|---|
items | Array<{ label?: string; value: string; subLabel?: string }> | Required — values must be unique. label is optional (omit for a sub-label-only row); subLabel is an optional secondary line under the label |
variableName | string | Context key — selected values stored as JSON.stringify(string[]), label as comma-joined display names |
defaultValues | string[] | Pre-selects matching items on mount; must reference valid item values |
direction | "vertical" | "horizontal" | Layout direction; defaults to "vertical" |
gap | number | Space between items; defaults to 8 |
itemBackgroundColor | string | Unselected item background; defaults to "transparent" |
itemSelectedBackgroundColor | string | Selected item background; defaults to primary + 10% opacity |
itemBorderColor | string | Unselected item border; defaults to theme.colors.neutral.low |
itemSelectedBorderColor | string | Selected item border; defaults to theme.colors.primary |
itemBorderRadius | number | Defaults to 8 |
itemBorderWidth | number | Defaults to 1 |
itemColor | string | Unselected label color; defaults to theme.colors.text.primary |
itemSelectedColor | string | Selected label color; defaults to theme.colors.primary |
showTick | boolean | Show/hide the checkbox box indicator. When false, the label and selected background/border still render. Defaults to true |
tickPosition | "start" | "end" | Tick placement relative to the label; defaults to "start" |
tickColor | string | Tick border color when unselected; defaults to theme.colors.neutral.medium |
tickSelectedColor | string | Tick border + fill color when selected; defaults to theme.colors.primary |
tickBorderRadius | number | Corner radius of the tick element; defaults to 4 |
tickSize | number | Side length of the tick box in px; the checkmark scales with it; defaults to 20 |
haptic | "none" | "light" | "medium" | "heavy" | "soft" | "rigid" | Tactile feedback fired on toggle. Maps to expo-haptics ImpactFeedbackStyle. Opt-in — omit or "none" for no feedback. Requires the optional expo-haptics peer dep (no-op if not installed) |
itemFontSize | number | |
itemFontWeight | string | |
itemFontFamily | string | |
itemSubLabelColor | string | Sub-label color when unselected; defaults to theme.colors.text.secondary |
itemSelectedSubLabelColor | string | Sub-label color when selected; falls back to itemSubLabelColor then the selected label color |
itemSubLabelFontSize | number | Defaults to the caption text-style size |
itemSubLabelFontWeight | string | |
itemSubLabelFontFamily | string | Falls back to itemFontFamily, then the theme font |
itemSubLabelFontStyle | "normal" | "italic" | |
itemPadding | number | Defaults to 12 when no itemPaddingHorizontal/Vertical is set |
itemPaddingHorizontal / itemPaddingVertical | number | |
itemShadowColor | string | Per-item iOS drop shadow color. Setting only this defaults itemShadowOpacity to 1, itemShadowRadius to 4 |
itemShadowOffset | { width: number; height: number } | Per-item shadow offset |
itemShadowOpacity | number | 0–1 |
itemShadowRadius | number | Shadow blur radius |
itemElevation | number | Per-item Android elevation shadow |
alignSelf | "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline" | |
width / height | number | Container dimensions |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth / borderRadius / borderColor | number | string | Container border |
opacity | number |
Button props:
| Prop | Type | Notes |
|---|---|---|
label | string | Required — button text |
variant | "filled" | "outlined" | "ghost" | Visual style; filled = solid primary background, outlined = border only, ghost = no background or border |
actions | ButtonAction[] | Ordered list run on press. See Button actions below. When omitted (and no legacy action), the press is a no-op |
action | "continue" | Deprecated — back-compat alias. When actions is absent and action === "continue", runtime treats it as actions: ["continue"] |
backgroundColor | string | Overrides variant background color |
color | string | Label text color |
fontSize | number | |
fontWeight | string | |
fontFamily | string | |
textAlign | "left" | "center" | "right" | |
alignSelf | "auto" | "flex-start" | "center" | "flex-end" | "stretch" | |
width / height | number | |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth / borderRadius / borderColor | number | string | |
opacity | number | |
shadowColor / shadowOffset / shadowOpacity / shadowRadius / elevation | string / { width, height } / number / number / number | iOS shadow + Android elevation. From BaseBoxProps — accepted on every element, applied by the Button renderer |
disabledWhen | LeafCondition | ConditionGroup | When truthy against current variables, blocks press actions and renders the disabled style |
disabledBackgroundColor / disabledColor | string | Deprecated — use disabledStyle. Fallback only when disabledStyle is absent |
pressedStyle | Partial<ButtonProps> | Style overrides merged while the button is held. Cannot nest pressedStyle/disabledStyle |
disabledStyle | Partial<ButtonProps> | Style overrides merged while disabled. Supersedes disabledBackgroundColor/disabledColor |
transitionDurationMs | number | Opacity animation duration (ms) between rest/pressed/disabled. Default 150 |
haptic | "none" | "light" | "medium" | "heavy" | "soft" | "rigid" | Tactile feedback fired on press (before actions run). Maps to expo-haptics ImpactFeedbackStyle. Opt-in — omit or "none" for no feedback. Requires the optional expo-haptics peer dep (no-op if not installed) |
Per-state styling
pressedStyle and disabledStyle are partial overrides merged on top of the base props for their state. Each accepts the overridable visual props (BaseBoxProps including shadow, plus variant, backgroundColor, color, fontSize, fontWeight, fontFamily, fontStyle, textAlign). The renderer animates opacity between states over transitionDurationMs; non-opacity props (color, shadow) switch instantly.
{
"type": "Button",
"props": {
"label": "Continue",
"actions": ["continue"],
"backgroundColor": "#6C63FF",
"shadowColor": "#000",
"shadowOffset": { "width": 0, "height": 4 },
"shadowOpacity": 0.18,
"shadowRadius": 8,
"elevation": 4,
"transitionDurationMs": 180,
"pressedStyle": { "opacity": 0.7, "backgroundColor": "#4f46e5" },
"disabledStyle": { "backgroundColor": "#d1d5db", "color": "#6b7280", "shadowOpacity": 0, "elevation": 0 }
}
}
Button actions
Button.actions is an ordered array. Each entry is one of:
"continue"— advances to the next onboarding step (calls theonContinuecallback wired to the renderer).{ type: "custom", function: string, variables?: string[] }— invokes a developer-injected handler registered onOnboardingProvider.customActions.variableslists the keys to read from the live ComposableScreen variable map and forward to the handler.{ type: "setVariable", name: string, value: string, label?: string, valueMode?: "literal" | "expression", kind?: "int" | "float" | "string", arrayOp?: "append" | "remove" | "toggle" }— writes to the variable map ({ value, label }). Pair with"continue"to set a discriminator a downstreamnextSteprule can branch on; the value is visible to the same-tick branch evaluation. WitharrayOpthe target is treated as aCheckboxGroup-style multi-select (JSON-encodedstring[]) andvalue/labelare the single member toappend(add, dedup),remove(drop), ortoggle(flip — matches a checkbox tap); the stored label stays comma-joined like a real checkbox andkindis ignored. This lets any element (viaonPress) add or remove a chip from a multi-select without aCheckboxGroupwidget.
Execution rules:
- Sequential. Each action awaits the previous before starting.
- Async-aware. If a custom handler returns a
Promise, the runtime awaits it. "continue"is terminal — any actions after it are ignored.- A thrown error in a custom handler is logged via
console.errorand the remaining chain is aborted. - An unknown
functionname (no matching key incustomActions) emitsconsole.warnand the chain skips to the next action.
{
"id": "primary-cta",
"type": "Button",
"props": {
"label": "Get Started",
"variant": "filled",
"actions": [
{ "type": "custom", "function": "trackCta", "variables": ["name", "plan"] },
"continue"
]
}
}
Register the matching handlers on OnboardingProvider:
import { OnboardingProvider } from "@rocapine/react-native-onboarding";
<OnboardingProvider
client={client}
customActions={{
trackCta: async ({ variables }) => {
// variables → { name?: { value, label? }, plan?: { value, label? } }
await analytics.track("cta_pressed", {
name: variables.name?.value,
plan: variables.plan?.value,
});
},
}}
>
{children}
</OnboardingProvider>
See Custom Actions for the full handler signature and patterns.
DatePicker props:
| Prop | Type | Notes |
|---|---|---|
variableName | string | Context key — selected date stored as ISO 8601 string (value) and locale-formatted string (label, e.g. "Apr 23, 2026") |
defaultValue | string | ISO date string, or "now" for the current date/time at render; used as initial value if no persisted value exists |
minimumDate | string | ISO date string, or "now" for the current date/time at render — earliest selectable date |
maximumDate | string | ISO date string, or "now" for the current date/time at render — latest selectable date |
mode | "date" | "time" | "datetime" | Defaults to "date" |
display | "default" | "spinner" | "calendar" | "clock" | "compact" | "inline" | Platform-specific display style; iOS defaults to "spinner", Android defaults to "default" |
textColor | string | Picker text color; defaults to theme.colors.text.primary |
accentColor | string | Picker accent/highlight color; defaults to theme.colors.primary |
locale | string | BCP 47 locale tag (e.g. "fr-FR") |
alignSelf | "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline" | |
width / height | number | Container dimensions |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth / borderRadius / borderColor | number | string | Container border |
opacity | number |
On Android, DatePicker renders as a Pressable showing the current value. Tapping opens the native modal picker. On iOS it renders inline.
Dependencies: @react-native-community/datetimepicker
npx expo install @react-native-community/datetimepicker
WheelPicker props:
Renders a scrolling wheel selector (native iOS wheel / Android dropdown) bound to a variable. Provide exactly one of items or range — supplying both, or neither, is a schema error.
| Prop | Type | Notes |
|---|---|---|
variableName | string | Context key — selected entry is written to shared composableVariables (value = item value, label = item label) |
defaultValue | string | Must match one of the available item values; seeds the variable on mount |
items | Array<{ label: string; value: string }> | Explicit options. Mutually exclusive with range; values must be unique |
range | { min: number; max: number; step?: number; unit?: string } | Numeric range that auto-generates options. step defaults to 1; unit is appended to each label as "<value> <unit>" (e.g. "70 kg"). Mutually exclusive with items |
itemColor | string | Text color of the wheel items; defaults to theme.colors.text.primary |
itemFontSize | number | Font size of the wheel items (iOS itemStyle) |
itemFontFamily | string | Font family of the wheel items (iOS itemStyle) |
alignSelf | "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline" | |
width / height | number | Container dimensions |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth / borderRadius / borderColor | number | string | Container border |
opacity | number |
Dependencies: @react-native-picker/picker (the same peer dep the Picker step type uses)
npx expo install @react-native-picker/picker
Slider props:
Renders a continuous numeric slider bound to a variable. The selected value is stored as a float string (kind: "float") in the variable — read it back downstream with a {{variableName}} expression or a renderWhen condition.
| Prop | Type | Notes |
|---|---|---|
variableName | string | Context key — the current value is written to shared composableVariables as a float string (value) |
defaultValue | number | Initial value; defaults to min (else 0). Seeds the variable on mount if variableName is set |
min | number | Minimum value; defaults to 0 |
max | number | Maximum value; defaults to 1 |
step | number | Step increment; 0 (default) means continuous |
minimumTrackTintColor | string | Color of the filled (left) portion of the track; defaults to theme.colors.primary |
maximumTrackTintColor | string | Color of the remaining (right) portion of the track |
thumbTintColor | string | Color of the draggable thumb |
disabled | boolean | Disables interaction |
All BaseBoxProps | — | width, height, margin*, padding*, alignSelf, opacity, etc. |
Dependencies: @react-native-community/slider (optional peer dep — the element degrades to an empty box when it is not installed)
npx expo install @react-native-community/slider
ProgressIndicator props:
Renders a linear bar or circular ring. By default the value range is 0–100 (a percentage), but minValue/maxValue let it represent any range — e.g. an animated count-up from 0 to N. The value can be static (value), bound to a variable (variableName), or animated on mount (autoplay). To show the count-up as plain text, set autoplay + variableName, then read {{var}} in a Text element (mode: "expression") — the variable carries the real value, no math needed.
| Prop | Type | Notes |
|---|---|---|
variant | "linear" | "circular" | Defaults to "linear" |
variableName | string | Bound variable. When autoplay is on, the current step-snapped value (in [minValue, maxValue]) is written each step hop; when off, the indicator reflects/animates to this variable's value |
value | number | Static value (in [minValue, maxValue]) used when no variableName is set |
autoplay | boolean | Animate from initialValue to maxValue on mount |
loop | boolean | Repeat the autoplay animation |
initialValue | number | Starting value (in [minValue, maxValue]); defaults to minValue |
minValue | number | Lower bound of the value range; defaults to 0 |
maxValue | number | Upper bound of the value range; defaults to 100 |
step | number (> 0) | Quantization step for the label/variable; defaults to 1. Bounds JS callbacks to (maxValue − minValue) / step per sweep — use a coarse step for large ranges (e.g. maxValue: 5000, step: 50) to avoid a per-step re-render storm |
labelSuffix | string | Appended after the label value; defaults to "%". Set "" or a unit (e.g. " kg") for non-percentage ranges |
duration | number | Animation duration in ms; defaults to 1000 |
delay | number | Delay in ms before the animation starts; defaults to 0 |
easing | "linear" | "ease-in" | "ease-out" | "ease-in-out" | Defaults to "ease-in-out" |
color | string | Progress fill color; defaults to theme.colors.primary |
trackColor | string | Track color; defaults to theme.colors.neutral.lower |
thickness | number | Bar height (linear) / ring stroke width (circular) |
size | number | Circular diameter in px; defaults to 120 |
showLabel | boolean | Show the value label (value + labelSuffix) |
labelColor | string | Label text color; defaults to theme.colors.text.primary |
All BaseBoxProps | — | width, height, margin*, padding*, alignSelf, opacity, etc. |
Dependencies: None
AnimatedText props:
Animates a number from from to to and renders it as formatted text. The animation runs entirely on the UI thread (reanimated) and writes the value straight into a native TextInput — so it produces no React re-render per frame and never touches the variable store. This is the performant way to show an animated stat (e.g. "+1,028,709 members"); it renders the number only, so compose any static label as a sibling Text. Prefer it over an autoplay ProgressIndicator bound to a variable when all you need is an animated number — the variable-bound approach re-renders the whole screen on every step.
| Prop | Type | Notes |
|---|---|---|
to | number | Required. End value of the count |
from | number | Start value; defaults to 0 |
duration | number | Animation duration in ms; defaults to 1000 |
delay | number | Delay in ms before the count starts; defaults to 0 |
easing | "linear" | "ease-in" | "ease-out" | "ease-in-out" | Defaults to "ease-out" |
autoplay | boolean | Animate on mount; defaults to true |
loop | boolean | Repeat the count indefinitely; defaults to false |
decimals | number (int ≥ 0) | Decimal places in the displayed number; defaults to 0 |
thousandsSeparator | string | Grouping separator for the integer part (e.g. 1,028,709); defaults to ",". Set "" to disable grouping |
fontSize / fontWeight / fontFamily / fontStyle / color / textAlign / letterSpacing / lineHeight | — | Text styling, same as Text (color defaults to theme.colors.text.primary) |
All BaseBoxProps | — | width, height, margin*, padding*, alignSelf, opacity, etc. |
Dependencies: None (uses react-native-reanimated, already required by the UI package)
DrawingPad props:
A freehand drawing surface (signature, doodle, annotation). The user draws with a finger; on each completed stroke the drawing is serialized into the bound variable(s) — an SVG path string into variableName and/or a base64 image data URI into imageVariableName. Writing on stroke-end (not per touch-move) avoids a per-frame setVariable re-render storm. The SVG variable is compact/vector and re-rendered with react-native-svg; the image variable is a drop-in Image source / upload payload.
| Prop | Type | Notes |
|---|---|---|
variableName | string | Receives the SVG path d string; label = "Drawing captured" once drawn (empty string when cleared) |
imageVariableName | string | Receives a base64 data URI (data:image/png;base64,…). Omit if you only need the SVG |
strokeColor | string | Pen color; defaults to theme.colors.text.primary |
strokeWidth | number (> 0) | Pen width in px; defaults to 2 |
backgroundColor | string | Canvas fill (also painted into the exported image); defaults to theme.colors.neutral.lowest |
clearable | boolean | Show a clear button once drawn; defaults to true |
clearButtonPosition | "top-left" | "top-right" | "bottom-left" | "bottom-right" | Corner the clear button sits in; defaults to "top-right" |
clearButtonOffset | number | Distance (px) from the two nearest edges; defaults to 8 |
clearButtonSize | number | Clear button diameter in px; defaults to 32 (glyph scales to half) |
clearButtonColor | string | Clear button background; defaults to theme.colors.neutral.higher |
clearButtonIconColor | string | Clear button glyph color; defaults to theme.colors.text.opposite |
clearButtonLabel | string | Clear button glyph/label; defaults to "✕" |
imageFormat | "png" | "jpeg" | Encode format for imageVariableName; defaults to "png" |
All BaseBoxProps | — | width, height (defaults to 200), borderWidth (default 2), borderRadius (default 16), borderColor (default theme.colors.primary), margin*, padding*, alignSelf, opacity, etc. |
Dependencies: @shopify/react-native-skia (mandatory for this element — used for both the live canvas and serialization; throws an explicit install error if missing)
npx expo install @shopify/react-native-skia
Carousel props:
| Prop | Type | Notes |
|---|---|---|
carouselType | "normal" | "left-align" | "parallax" | "stack" | Layout mode — see below; defaults to "normal" |
autoPlay | boolean | Auto-advance slides; defaults to false |
autoPlayInterval | number | Milliseconds between auto-advances; defaults to 3000 |
loop | boolean | Loop back to first slide after last; defaults to true |
showDots | boolean | Show Pagination.Basic pill dots; defaults to true |
dotColor | string | Inactive dot color; defaults to theme.colors.neutral.low |
activeDotColor | string | Active dot color; defaults to theme.colors.primary |
dotWidth / dotHeight | number | Inactive dot size; default 20 / 4 |
activeDotWidth / activeDotHeight | number | Active dot size; default to dotWidth / dotHeight when unset |
dotsGap | number | Gap between dots; defaults to 8 |
dotsPosition | "top" | "bottom" | Render dot row above or below the carousel; defaults to "bottom" |
dotsMarginTop / dotsMarginBottom | number | Dot-row margins; default 12 / 0 |
height | number | Slide height in dp; defaults to 220 |
width | number | Slide width; defaults to useWindowDimensions().width (overridden for stack and left-align) |
borderRadius | number | Applied to each slide's inner View |
All BaseBoxProps | — | margin, padding, alignSelf, borderColor, borderWidth, opacity, etc. applied to outer container |
carouselType modes:
| Mode | Width | Library prop | Description |
|---|---|---|---|
"normal" | 100 % window | — | Full-width paged carousel |
"parallax" | 100 % window | mode="parallax" | Depth-zoom effect on adjacent slides |
"stack" | 75 % window | mode="horizontal-stack" | Stacked card effect; multiple slides visible |
"left-align" | 82 % window | — | Next slide peeks in from the right; overflow: "visible" on container |
Slides are arbitrary UIElement trees — place any renderable element (Image, Text, YStack, etc.) as children. Each child becomes one slide.
Dependencies: react-native-reanimated-carousel
npx expo install react-native-reanimated-carousel
Variable context
Input, RadioGroup, CheckboxGroup, DatePicker, WheelPicker, Slider, and DrawingPad elements write into a shared composableVariables map stored in OnboardingProgressContext. This state persists across navigation between ComposableScreen steps, so values collected on an early screen are available on later screens.
Each entry is a structured object { value: string; label?: string }:
| Element | value | label |
|---|---|---|
Input | Raw text string | — |
RadioGroup | Selected item value (e.g. "monthly") | Selected item label (e.g. "Monthly") |
CheckboxGroup | JSON.stringify(string[]) of selected values | Comma-joined display labels (e.g. "Health, Fitness") |
DatePicker | ISO 8601 string (e.g. "1990-01-01T00:00:00.000Z") | Locale-formatted date (e.g. "Jan 1, 1990") |
WheelPicker | Selected item value (e.g. "70") | Selected item label (e.g. "70 kg") |
DrawingPad | SVG path string (variableName) / base64 image data URI (imageVariableName) | "Drawing captured" when drawn (SVG var only) |
Slider | Current value as a float string (e.g. "0.5") | — |
Text elements with mode: "expression" interpolate {{variableName}} patterns in their content string from that map. The renderer prefers label over value when both are present. If a key has no value yet, the pattern is replaced with an empty string.
[
{
"id": "name-input",
"type": "Input",
"props": {
"variableName": "name",
"placeholder": "Enter your name",
"autoCapitalize": "words",
"borderRadius": 12
}
},
{
"id": "greeting",
"type": "Text",
"props": {
"content": "Hello {{name}}!",
"mode": "expression",
"fontSize": 18,
"fontWeight": "600",
"textAlign": "center"
}
}
]
OnboardingProgressProvider must wrap your app for the variable context to work. If you use @rocapine/react-native-onboarding-ui, wrap your root layout with it:
import { OnboardingProgressProvider } from "@rocapine/react-native-onboarding-ui";
export default function RootLayout() {
return (
<OnboardingProgressProvider>
{/* ... */}
</OnboardingProgressProvider>
);
}
Without the provider the context falls back to its default (empty variables, no-op setter) — Input values will not be saved and expression Text will render blank for any {{variable}}.
Conditional rendering (renderWhen)
Every UIElement variant accepts an optional renderWhen field — a
LeafCondition or ConditionGroup evaluated against the live variable
map. When the condition is false the element (and its entire subtree
for containers) is skipped — no node mounts, no layout cost, no
side-effects from Lottie / Video / Input.
Same condition schema as Button.disabledWhen and nextStep.branches[].condition:
{
"id": "lifetime-badge",
"renderWhen": {
"variable": "plan",
"operator": "eq",
"value": "lifetime"
},
"type": "Text",
"props": {
"content": "🎉 Best value — lifetime access!",
"fontSize": 14,
"fontWeight": "600",
"textAlign": "center"
}
}
Use ConditionGroup for AND / OR composition:
{
"id": "yearly-or-lifetime-note",
"renderWhen": {
"logic": "or",
"conditions": [
{ "variable": "plan", "operator": "eq", "value": "yearly" },
{ "variable": "plan", "operator": "eq", "value": "lifetime" }
]
},
"type": "Text",
"props": { "content": "Best long-term value" }
}
Operators. Binary operators compare the variable to value (required):
eq, neq, gt, lt, gte, lte, contains, in, not_in. Unary
operators test the variable alone and take no value:
| Operator | True when |
|---|---|
is_empty | value is an empty/whitespace string, an empty array, or unset/null |
is_not_empty | negation of is_empty |
is_null | value is unset or null only (a set-but-empty "" is not null) |
is_not_null | negation of is_null |
{ "id": "greeting", "renderWhen": { "variable": "name", "operator": "is_not_empty" }, "type": "Text", "props": { "content": "Hi {{name}}!", "mode": "expression" } }
Prefer is_not_empty over { "operator": "neq", "value": "" } — it is
type-aware and reads clearly. A binary operator missing value is a schema
error; a unary operator ignores any value.
Element-level defaults (Carousel.defaultIndex, RadioGroup.defaultValue,
CheckboxGroup.defaultValues, Input.defaultValue,
DatePicker.defaultValue, WheelPicker.defaultValue, Slider.defaultValue) are overlaid onto the variable map
synchronously on first render — so renderWhen and {{var}}
interpolation see defaults from the very first frame, before any user
interaction.
renderWhen vs Branch.conditionrenderWhen— hides a single element inside a screen.Branch.condition(step.nextStep.branches[].condition) — picks the next step in the flow when continue is pressed.
Pick renderWhen for in-screen toggles (validation text, plan-specific
copy, optional sections). Pick Branch.condition to skip whole steps.
Icon props:
| Prop | Type | Notes |
|---|---|---|
name | string | Required — Lucide icon name e.g. "Star", "Heart", "CheckCircle" |
size | number | Icon width & height in dp; defaults to 24 |
color | string | Stroke color; defaults to theme.colors.text.primary |
strokeWidth | number | Line stroke weight; defaults to 2 |
fill | string | Fill color (any CSS color). Omit ⇒ Lucide default "none" (outlined). Set to the same value as color for a fully filled icon, or to a tinted color for a soft look. Works best with closed-path icons (Star, Heart, Bookmark, Circle, CheckCircle2). |
fillOpacity | number | 0–1. Lower values tint the fill against the stroke. Defaults to 1. |
width / height | number | Wrapper View dimensions |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth | number | |
borderRadius | number | |
borderColor | string | |
opacity | number |
Icon uses Lucide React Native, which is bundled with the UI package — no extra install needed. If an unknown icon name is passed the element renders nothing.
Lottie props:
| Prop | Type | Notes |
|---|---|---|
source | string | Required — remote .json URL |
width | number | Defaults to "100%" |
height | number | Defaults to 200 |
autoPlay | boolean | Defaults to true |
loop | boolean | Defaults to true |
speed | number | Playback speed multiplier |
opacity | number | |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth / borderRadius / borderColor | number | string | Applied via a wrapping View |
Dependencies: lottie-react-native
npx expo install lottie-react-native
If lottie-react-native is not installed, the element renders a placeholder view with an install hint instead of crashing.
Rive props:
| Prop | Type | Notes |
|---|---|---|
url | string | Required — remote .riv URL |
width | number | Defaults to "100%" |
height | number | Optional. When omitted, aspectRatio sizes the box |
aspectRatio | number | Used when height is absent; falls back to 1 if neither height / flex / aspectRatio / minHeight / maxHeight is set, because rive-react-native doesn't expose the artboard's intrinsic ratio to JS |
autoplay | boolean | Defaults to true |
fit | "Contain" | "Cover" | "Fill" | "FitWidth" | "FitHeight" | "None" | "ScaleDown" | "Layout" | |
alignment | "TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight" | |
artboardName | string | Target artboard in the .riv file |
stateMachineName | string | State machine to drive |
opacity | number | |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth / borderRadius / borderColor | number | string | Applied via a wrapping View |
Dependencies: rive-react-native
npx expo install rive-react-native
If rive-react-native is not installed, the element renders a placeholder view with an install hint instead of crashing.
Video props:
| Prop | Type | Notes |
|---|---|---|
url | string | Required — remote video URL |
width | number | Defaults to "100%" |
height | number | Defaults to 200 |
autoPlay | boolean | Start playing on mount; defaults to false |
loop | boolean | Loop playback; defaults to false |
muted | boolean | Mute audio; defaults to true (required for autoplay on iOS) |
controls | boolean | Show native playback controls; defaults to false |
opacity | number | |
margin / marginHorizontal / marginVertical | number | |
padding / paddingHorizontal / paddingVertical | number | |
borderWidth | number | Applied via a wrapping View |
borderRadius | number | Applied via a wrapping View |
borderColor | string | Applied via a wrapping View |
Dependencies: expo-video
npx expo install expo-video
If expo-video is not installed, the element renders a placeholder view with an install hint instead of crashing.
Payload:
{
elements: UIElement[]; // Recursive tree of layout and media elements
}
Example:
{
"id": "feature-highlight",
"type": "ComposableScreen",
"payload": {
"elements": [
{
"id": "card",
"type": "YStack",
"props": {
"padding": 24,
"gap": 12,
"borderWidth": 1,
"borderRadius": 16,
"borderColor": "#E0E0E0",
"overflow": "hidden"
},
"children": [
{
"id": "hero",
"type": "Image",
"props": {
"url": "https://example.com/hero.png",
"height": 180,
"resizeMode": "cover",
"borderRadius": 12
}
},
{
"id": "title",
"type": "Text",
"props": {
"content": "Welcome aboard",
"fontSize": 24,
"fontWeight": "700"
}
},
{
"id": "row",
"type": "XStack",
"props": { "gap": 8, "alignItems": "center" },
"children": [
{
"id": "badge",
"type": "YStack",
"props": {
"backgroundColor": "#E8F5E9",
"borderRadius": 8,
"padding": 8
},
"children": [
{
"id": "badge-text",
"type": "Text",
"props": { "content": "New", "fontSize": 12, "color": "#2E7D32" }
}
]
},
{
"id": "label",
"type": "Text",
"props": { "content": "Personalized for you", "fontSize": 14 }
}
]
}
]
}
]
}
}
ZStack example — image with text overlay:
{
"id": "hero-overlay",
"type": "ZStack",
"props": {
"height": 200,
"borderRadius": 16,
"overflow": "hidden",
"marginVertical": 8
},
"children": [
{
"id": "bg",
"type": "Image",
"props": { "url": "https://example.com/hero.jpg", "height": 200, "resizeMode": "cover" }
},
{
"id": "overlay",
"type": "YStack",
"props": { "backgroundColor": "rgba(0,0,0,0.45)", "padding": 20, "justifyContent": "flex-end" },
"children": [
{
"id": "label",
"type": "Text",
"props": { "content": "Your headline here", "fontSize": 18, "fontWeight": "700", "color": "#fff" }
}
]
}
]
}
Dependencies: None for YStack, XStack, ZStack, Text, Image, Icon, Input, RadioGroup, CheckboxGroup, Button, ProgressIndicator. The optional haptic prop on Button / RadioGroup / CheckboxGroup uses expo-haptics (optional peer dep) — it silently no-ops if not installed, so no dependency is required unless you opt into haptics. SafeAreaView requires react-native-safe-area-context (already a peer dep of @rocapine/react-native-onboarding-ui). See the Carousel, Lottie, Rive, Video, DatePicker, WheelPicker, and Slider sections above for elements with peer deps.
Common Properties
All page types share these properties:
{
id: string; // Unique identifier
type: string; // Discriminated union field
name: string; // Display name (for debugging)
displayProgressHeader: boolean; // Show/hide progress bar
payload: object; // Type-specific data
customPayload: object | null; // Your custom fields
continueButtonLabel?: string; // Optional CTA text
figmaUrl?: string | null; // Design reference
}
Customization Levels
Each page type supports three levels of customization:
Level 1: Theming
Apply theme tokens to all screens:
<OnboardingProvider
theme={{
colors: { primary: "#FF5733" },
typography: { fontFamily: { title: "CustomFont" } }
}}
/>
Level 2: Custom Components
Replace specific UI components (currently available for Question type):
<OnboardingProvider
customComponents={{
QuestionAnswerButton: MyCustomButton
}}
/>
Learn more about custom components →
Level 3: Custom Renderers
Complete control over specific screens:
// In your routing file
if (step.id === "custom-screen") {
return <MyCustomRenderer step={step} onContinue={onContinue} />;
}
Learn more about custom renderers →
Media Support
Several page types support media with the MediaSourceSchema:
{
type: "video" | "image" | "lottie" | "rive";
url?: string; // Remote URL
localPathId?: string; // Local asset path
}
Supported media types:
- video: Video files (implementation pending)
- image: PNG, JPG, WebP via React Native Image
- lottie: Lottie animations via
lottie-react-native - rive: Rive animations via
rive-react-native
Using Individual Renderers
You can use renderers directly without OnboardingPage:
import { MediaContentRenderer, MediaContentStepType } from "@rocapine/react-native-onboarding";
const step: MediaContentStepType = {
id: "welcome",
type: "MediaContent",
name: "Welcome",
displayProgressHeader: true,
payload: {
title: "Welcome!",
description: "Let's get started",
media: { type: "image", url: "https://..." },
},
customPayload: null,
continueButtonLabel: "Get Started",
figmaUrl: null,
};
<MediaContentRenderer step={step} onContinue={handleContinue} />
Next Steps
- 🎨 Customization Overview - Learn about customization levels
- 📘 API Reference - Complete type definitions
- 🚀 Getting Started - Build your first onboarding flow