Skip to main content

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


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 units
  • height - Height with cm/ft+in units
  • age - Age picker
  • gender - Gender selection
  • name - Name input
  • date - 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 padding and margin
  • 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 contextInput, RadioGroup, CheckboxGroup, DatePicker, WheelPicker, Slider, and DrawingPad elements write into shared state; Text elements 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:

ElementRenders asDirectionPeer dep
YStack<View>column
XStack<View>row
ZStack<View>depth (z-axis)
SafeAreaView<SafeAreaView>columnreact-native-safe-area-context
Text<Text>
RichText<View> wrapping Text childrenwrapping row
Image<Image>
ProgressiveBlurImage<Image> + masked blurred image copydepth (z-axis)@react-native-masked-view/masked-view, expo-linear-gradient, expo-image (all optional)
IconLucide icon— (bundled)
Input<TextInput>
RadioGroupRadio item list
CheckboxGroupCheckbox item list
Button<TouchableOpacity>
CarouselHorizontal slidesreact-native-reanimated-carousel
DatePickerNative date/time picker@react-native-community/datetimepicker
WheelPickerNative scrolling wheel selector@react-native-picker/picker
DrawingPadFreehand draw/signature canvas@shopify/react-native-skia
SliderContinuous numeric slider@react-native-community/slider
ProgressIndicatorLinear bar / circular ring
AnimatedTextCount-up number (UI-thread, native TextInput)
LottieLottieViewlottie-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-reanimatedpreset 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:

FieldTypeNotes
translateX / translateYnumberpx offset
scale / scaleX / scaleYnumber
rotatenumberdegrees

animation{ entering?, exiting?, layout?, effect? }:

Sub-fieldShapeNotes
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 over easing when 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: false runs 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):

PropTypeNotes
gapnumberSpace between children
padding / paddingHorizontal / paddingVerticalnumber
margin / marginHorizontal / marginVerticalnumber
flex / flexShrink / flexWrapnumber | stringflexShrink 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 / maxHeightnumber
backgroundColor / borderColorstring
borderWidth / borderRadiusnumber
overflow"hidden" | "visible" | "scroll"Use "hidden" to clip rounded corners
opacitynumber

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.

PropTypeNotes
width / height / minWidth / maxWidth / minHeight / maxHeightnumberExplicit height is recommended — absolute children do not size the parent
padding / paddingHorizontal / paddingVerticalnumberApplied to the ZStack container
margin / marginHorizontal / marginVerticalnumber
flex / flexShrink / flexGrownumberApply to the ZStack itself within a parent flex container
alignSelf"auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
backgroundColor / backgroundGradientstring / GradientBackground
borderWidth / borderRadius / borderColornumber | string
overflow"hidden" | "visible" | "scroll"Use "hidden" to clip overflowing children to rounded corners
opacitynumber
ZStack behavior

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.

PropTypeNotes
mode"padding" | "margin"Defaults to "padding". "margin" is useful when the SafeAreaView itself has a background that should not extend into the inset region
edgesEdge[] 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 BaseBoxPropsflex, 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": [ /* ... */ ] }
]
}
OnboardingTemplate no longer applies insets

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:

PropTypeNotes
urlstringRequired — remote image URI
widthnumberDefaults to "100%"
heightnumberWhen omitted, aspectRatio is used to size the image
aspectRationumberUsed when height is absent; defaults to 16/9
resizeMode"cover" | "contain" | "stretch" | "center"Defaults to "cover". For SVG sources it maps to preserveAspectRatio
blurRadiusnumberUniform Gaussian blur radius in px (native, no extra dep). 0/omitted = sharp. Ignored for SVGs
borderRadius / borderWidthnumber
borderColorstring
opacitynumber
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
Image format support
  • WebP / AVIF: when the optional peer dep expo-image is installed, Image renders through it for reliable WebP/AVIF decoding (React Native's built-in Image is unreliable for WebP on iOS). Without expo-image, it falls back to React Native's Image.
  • SVG: a url whose path ends in .svg (query string / hash tolerant) is auto-detected and rendered with react-native-svg's SvgUri — no schema change, existing .svg URLs just work. resizeMode maps to the SVG preserveAspectRatio.

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.

PropTypeNotes
urlstringRequired — remote image URI
intensitynumberRequired — blur strength, 0100 (mapped to the blurred copy's blur radius)
masklinear { 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"
maxBlurOpacitynumberClamp 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
Optional peer deps + graceful degradation

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:

PropTypeNotes
contentstring | 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 / lineHeightnumber | string
fontFamilystringFont family name; must be loaded by the host app (e.g. via expo-font)
colorstringDefaults to theme.colors.text.primary
textAlign"left" | "center" | "right"
backgroundColor / borderColorstring
padding / paddingHorizontal / paddingVerticalnumber
margin / marginHorizontal / marginVerticalnumber
borderWidth / borderRadius / opacitynumber

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 propTypeNotes
textstringRequired. The span's text (interpolated in mode: "expression")
fontWeightstring
fontStyle"normal" | "italic"
fontFamilystring | "inherit"Defaults to the parent Text's resolved family
fontSize / letterSpacingnumber
colorstring
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.

  • children are restricted to Text elements only (a non-Text child fails schema parse).
  • props are layout props + all BaseBoxProps + inherited text-style defaults. The text-style fields (fontSize, fontWeight, fontFamily, fontStyle, color, textAlign, letterSpacing, lineHeight) are handed down to every child Text — 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).
PropTypeNotes
flexWrap"wrap" | "nowrap"Defaults to "wrap"
gapnumberSpace 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 / lineHeightsame as TextInherited 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 (leftflex-start, centercenter, rightflex-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:

PropTypeNotes
variableNamestringContext key — value is written to shared composableVariables on every keystroke
placeholderstringPlaceholder text
placeholderColorstringDefaults to theme.colors.text.tertiary
defaultValuestringInitial 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"
secureTextEntrybooleanMask input for passwords; defaults to false
maxLengthnumberMaximum character count
multilinebooleanEnable multi-line input; defaults to false
numberOfLinesnumberVisible line count when multiline is true
editablebooleanDefaults to true
autoFocusbooleanFocus on mount and open the keyboard; defaults to false
colorstringText color; defaults to theme.colors.text.primary
fontSizenumberDefaults to 16
textAlign"left" | "center" | "right"
padding / paddingHorizontal / paddingVerticalnumberInner padding; defaults to 12
backgroundColorstringDefaults to theme.colors.neutral.lowest
borderWidthnumberDefaults to 1
borderRadiusnumberDefaults to 8
borderColorstringDefaults to theme.colors.neutral.low
alignSelf"auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
width / heightnumber
opacitynumber
margin / marginHorizontal / marginVerticalnumber

RadioGroup props:

PropTypeNotes
itemsArray<{ 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
variableNamestringContext key — selected entry (value + label) is written to shared composableVariables
defaultValuestringPre-selects the matching item on mount; also written to context
direction"vertical" | "horizontal"Layout direction; defaults to "vertical"
gapnumberSpace between items; defaults to 8
itemBackgroundColorstringUnselected item background; defaults to "transparent"
itemSelectedBackgroundColorstringSelected item background; defaults to primary + 15% opacity
itemBorderColorstringUnselected item border; defaults to theme.colors.neutral.low
itemSelectedBorderColorstringSelected item border; defaults to theme.colors.primary
itemBorderRadiusnumberDefaults to 8
itemBorderWidthnumberDefaults to 1
itemColorstringUnselected label color; defaults to theme.colors.text.primary
itemSelectedColorstringSelected label color; defaults to theme.colors.primary
showTickbooleanShow/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"
tickColorstringTick color (border + dot) when unselected; defaults to theme.colors.neutral.medium
tickSelectedColorstringTick color (border + dot) when selected; defaults to theme.colors.primary
tickBorderRadiusnumberCorner radius of the tick element; defaults to tickSize / 2 (full circle)
tickSizenumberDiameter 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)
itemFontSizenumber
itemFontWeightstring
itemFontFamilystring
itemSubLabelColorstringSub-label color when unselected; defaults to theme.colors.text.secondary
itemSelectedSubLabelColorstringSub-label color when selected; falls back to itemSubLabelColor then the selected label color
itemSubLabelFontSizenumberDefaults to the caption text-style size
itemSubLabelFontWeightstring
itemSubLabelFontFamilystringFalls back to itemFontFamily, then the theme font
itemSubLabelFontStyle"normal" | "italic"
itemPaddingnumberDefaults to 12 when no itemPaddingHorizontal/Vertical is set
itemPaddingHorizontal / itemPaddingVerticalnumber
itemShadowColorstringPer-item iOS drop shadow color. Setting only this defaults itemShadowOpacity to 1, itemShadowRadius to 4
itemShadowOffset{ width: number; height: number }Per-item shadow offset
itemShadowOpacitynumber01
itemShadowRadiusnumberShadow blur radius
itemElevationnumberPer-item Android elevation shadow
alignSelf"auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
width / heightnumberContainer dimensions
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidth / borderRadius / borderColornumber | stringContainer border
opacitynumber

CheckboxGroup props:

PropTypeNotes
itemsArray<{ 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
variableNamestringContext key — selected values stored as JSON.stringify(string[]), label as comma-joined display names
defaultValuesstring[]Pre-selects matching items on mount; must reference valid item values
direction"vertical" | "horizontal"Layout direction; defaults to "vertical"
gapnumberSpace between items; defaults to 8
itemBackgroundColorstringUnselected item background; defaults to "transparent"
itemSelectedBackgroundColorstringSelected item background; defaults to primary + 10% opacity
itemBorderColorstringUnselected item border; defaults to theme.colors.neutral.low
itemSelectedBorderColorstringSelected item border; defaults to theme.colors.primary
itemBorderRadiusnumberDefaults to 8
itemBorderWidthnumberDefaults to 1
itemColorstringUnselected label color; defaults to theme.colors.text.primary
itemSelectedColorstringSelected label color; defaults to theme.colors.primary
showTickbooleanShow/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"
tickColorstringTick border color when unselected; defaults to theme.colors.neutral.medium
tickSelectedColorstringTick border + fill color when selected; defaults to theme.colors.primary
tickBorderRadiusnumberCorner radius of the tick element; defaults to 4
tickSizenumberSide 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)
itemFontSizenumber
itemFontWeightstring
itemFontFamilystring
itemSubLabelColorstringSub-label color when unselected; defaults to theme.colors.text.secondary
itemSelectedSubLabelColorstringSub-label color when selected; falls back to itemSubLabelColor then the selected label color
itemSubLabelFontSizenumberDefaults to the caption text-style size
itemSubLabelFontWeightstring
itemSubLabelFontFamilystringFalls back to itemFontFamily, then the theme font
itemSubLabelFontStyle"normal" | "italic"
itemPaddingnumberDefaults to 12 when no itemPaddingHorizontal/Vertical is set
itemPaddingHorizontal / itemPaddingVerticalnumber
itemShadowColorstringPer-item iOS drop shadow color. Setting only this defaults itemShadowOpacity to 1, itemShadowRadius to 4
itemShadowOffset{ width: number; height: number }Per-item shadow offset
itemShadowOpacitynumber01
itemShadowRadiusnumberShadow blur radius
itemElevationnumberPer-item Android elevation shadow
alignSelf"auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
width / heightnumberContainer dimensions
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidth / borderRadius / borderColornumber | stringContainer border
opacitynumber

Button props:

PropTypeNotes
labelstringRequired — button text
variant"filled" | "outlined" | "ghost"Visual style; filled = solid primary background, outlined = border only, ghost = no background or border
actionsButtonAction[]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"]
backgroundColorstringOverrides variant background color
colorstringLabel text color
fontSizenumber
fontWeightstring
fontFamilystring
textAlign"left" | "center" | "right"
alignSelf"auto" | "flex-start" | "center" | "flex-end" | "stretch"
width / heightnumber
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidth / borderRadius / borderColornumber | string
opacitynumber
shadowColor / shadowOffset / shadowOpacity / shadowRadius / elevationstring / { width, height } / number / number / numberiOS shadow + Android elevation. From BaseBoxProps — accepted on every element, applied by the Button renderer
disabledWhenLeafCondition | ConditionGroupWhen truthy against current variables, blocks press actions and renders the disabled style
disabledBackgroundColor / disabledColorstringDeprecated — use disabledStyle. Fallback only when disabledStyle is absent
pressedStylePartial<ButtonProps>Style overrides merged while the button is held. Cannot nest pressedStyle/disabledStyle
disabledStylePartial<ButtonProps>Style overrides merged while disabled. Supersedes disabledBackgroundColor/disabledColor
transitionDurationMsnumberOpacity 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 the onContinue callback wired to the renderer).
  • { type: "custom", function: string, variables?: string[] } — invokes a developer-injected handler registered on OnboardingProvider.customActions. variables lists 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 downstream nextStep rule can branch on; the value is visible to the same-tick branch evaluation. With arrayOp the target is treated as a CheckboxGroup-style multi-select (JSON-encoded string[]) and value/label are the single member to append (add, dedup), remove (drop), or toggle (flip — matches a checkbox tap); the stored label stays comma-joined like a real checkbox and kind is ignored. This lets any element (via onPress) add or remove a chip from a multi-select without a CheckboxGroup widget.

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.error and the remaining chain is aborted.
  • An unknown function name (no matching key in customActions) emits console.warn and 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:

PropTypeNotes
variableNamestringContext key — selected date stored as ISO 8601 string (value) and locale-formatted string (label, e.g. "Apr 23, 2026")
defaultValuestringISO date string, or "now" for the current date/time at render; used as initial value if no persisted value exists
minimumDatestringISO date string, or "now" for the current date/time at render — earliest selectable date
maximumDatestringISO 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"
textColorstringPicker text color; defaults to theme.colors.text.primary
accentColorstringPicker accent/highlight color; defaults to theme.colors.primary
localestringBCP 47 locale tag (e.g. "fr-FR")
alignSelf"auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
width / heightnumberContainer dimensions
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidth / borderRadius / borderColornumber | stringContainer border
opacitynumber
Android

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.

PropTypeNotes
variableNamestringContext key — selected entry is written to shared composableVariables (value = item value, label = item label)
defaultValuestringMust match one of the available item values; seeds the variable on mount
itemsArray<{ 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
itemColorstringText color of the wheel items; defaults to theme.colors.text.primary
itemFontSizenumberFont size of the wheel items (iOS itemStyle)
itemFontFamilystringFont family of the wheel items (iOS itemStyle)
alignSelf"auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline"
width / heightnumberContainer dimensions
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidth / borderRadius / borderColornumber | stringContainer border
opacitynumber

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.

PropTypeNotes
variableNamestringContext key — the current value is written to shared composableVariables as a float string (value)
defaultValuenumberInitial value; defaults to min (else 0). Seeds the variable on mount if variableName is set
minnumberMinimum value; defaults to 0
maxnumberMaximum value; defaults to 1
stepnumberStep increment; 0 (default) means continuous
minimumTrackTintColorstringColor of the filled (left) portion of the track; defaults to theme.colors.primary
maximumTrackTintColorstringColor of the remaining (right) portion of the track
thumbTintColorstringColor of the draggable thumb
disabledbooleanDisables interaction
All BaseBoxPropswidth, 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.

PropTypeNotes
variant"linear" | "circular"Defaults to "linear"
variableNamestringBound 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
valuenumberStatic value (in [minValue, maxValue]) used when no variableName is set
autoplaybooleanAnimate from initialValue to maxValue on mount
loopbooleanRepeat the autoplay animation
initialValuenumberStarting value (in [minValue, maxValue]); defaults to minValue
minValuenumberLower bound of the value range; defaults to 0
maxValuenumberUpper bound of the value range; defaults to 100
stepnumber (> 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
labelSuffixstringAppended after the label value; defaults to "%". Set "" or a unit (e.g. " kg") for non-percentage ranges
durationnumberAnimation duration in ms; defaults to 1000
delaynumberDelay in ms before the animation starts; defaults to 0
easing"linear" | "ease-in" | "ease-out" | "ease-in-out"Defaults to "ease-in-out"
colorstringProgress fill color; defaults to theme.colors.primary
trackColorstringTrack color; defaults to theme.colors.neutral.lower
thicknessnumberBar height (linear) / ring stroke width (circular)
sizenumberCircular diameter in px; defaults to 120
showLabelbooleanShow the value label (value + labelSuffix)
labelColorstringLabel text color; defaults to theme.colors.text.primary
All BaseBoxPropswidth, 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.

PropTypeNotes
tonumberRequired. End value of the count
fromnumberStart value; defaults to 0
durationnumberAnimation duration in ms; defaults to 1000
delaynumberDelay in ms before the count starts; defaults to 0
easing"linear" | "ease-in" | "ease-out" | "ease-in-out"Defaults to "ease-out"
autoplaybooleanAnimate on mount; defaults to true
loopbooleanRepeat the count indefinitely; defaults to false
decimalsnumber (int ≥ 0)Decimal places in the displayed number; defaults to 0
thousandsSeparatorstringGrouping separator for the integer part (e.g. 1,028,709); defaults to ",". Set "" to disable grouping
fontSize / fontWeight / fontFamily / fontStyle / color / textAlign / letterSpacing / lineHeightText styling, same as Text (color defaults to theme.colors.text.primary)
All BaseBoxPropswidth, 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.

PropTypeNotes
variableNamestringReceives the SVG path d string; label = "Drawing captured" once drawn (empty string when cleared)
imageVariableNamestringReceives a base64 data URI (data:image/png;base64,…). Omit if you only need the SVG
strokeColorstringPen color; defaults to theme.colors.text.primary
strokeWidthnumber (> 0)Pen width in px; defaults to 2
backgroundColorstringCanvas fill (also painted into the exported image); defaults to theme.colors.neutral.lowest
clearablebooleanShow 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"
clearButtonOffsetnumberDistance (px) from the two nearest edges; defaults to 8
clearButtonSizenumberClear button diameter in px; defaults to 32 (glyph scales to half)
clearButtonColorstringClear button background; defaults to theme.colors.neutral.higher
clearButtonIconColorstringClear button glyph color; defaults to theme.colors.text.opposite
clearButtonLabelstringClear button glyph/label; defaults to "✕"
imageFormat"png" | "jpeg"Encode format for imageVariableName; defaults to "png"
All BaseBoxPropswidth, 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:

PropTypeNotes
carouselType"normal" | "left-align" | "parallax" | "stack"Layout mode — see below; defaults to "normal"
autoPlaybooleanAuto-advance slides; defaults to false
autoPlayIntervalnumberMilliseconds between auto-advances; defaults to 3000
loopbooleanLoop back to first slide after last; defaults to true
showDotsbooleanShow Pagination.Basic pill dots; defaults to true
dotColorstringInactive dot color; defaults to theme.colors.neutral.low
activeDotColorstringActive dot color; defaults to theme.colors.primary
dotWidth / dotHeightnumberInactive dot size; default 20 / 4
activeDotWidth / activeDotHeightnumberActive dot size; default to dotWidth / dotHeight when unset
dotsGapnumberGap between dots; defaults to 8
dotsPosition"top" | "bottom"Render dot row above or below the carousel; defaults to "bottom"
dotsMarginTop / dotsMarginBottomnumberDot-row margins; default 12 / 0
heightnumberSlide height in dp; defaults to 220
widthnumberSlide width; defaults to useWindowDimensions().width (overridden for stack and left-align)
borderRadiusnumberApplied to each slide's inner View
All BaseBoxPropsmargin, padding, alignSelf, borderColor, borderWidth, opacity, etc. applied to outer container

carouselType modes:

ModeWidthLibrary propDescription
"normal"100 % windowFull-width paged carousel
"parallax"100 % windowmode="parallax"Depth-zoom effect on adjacent slides
"stack"75 % windowmode="horizontal-stack"Stacked card effect; multiple slides visible
"left-align"82 % windowNext 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 }:

Elementvaluelabel
InputRaw text string
RadioGroupSelected item value (e.g. "monthly")Selected item label (e.g. "Monthly")
CheckboxGroupJSON.stringify(string[]) of selected valuesComma-joined display labels (e.g. "Health, Fitness")
DatePickerISO 8601 string (e.g. "1990-01-01T00:00:00.000Z")Locale-formatted date (e.g. "Jan 1, 1990")
WheelPickerSelected item value (e.g. "70")Selected item label (e.g. "70 kg")
DrawingPadSVG path string (variableName) / base64 image data URI (imageVariableName)"Drawing captured" when drawn (SVG var only)
SliderCurrent 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"
}
}
]
note

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:

OperatorTrue when
is_emptyvalue is an empty/whitespace string, an empty array, or unset/null
is_not_emptynegation of is_empty
is_nullvalue is unset or null only (a set-but-empty "" is not null)
is_not_nullnegation 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.condition
  • renderWhen — 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:

PropTypeNotes
namestringRequired — Lucide icon name e.g. "Star", "Heart", "CheckCircle"
sizenumberIcon width & height in dp; defaults to 24
colorstringStroke color; defaults to theme.colors.text.primary
strokeWidthnumberLine stroke weight; defaults to 2
fillstringFill 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).
fillOpacitynumber0–1. Lower values tint the fill against the stroke. Defaults to 1.
width / heightnumberWrapper View dimensions
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidthnumber
borderRadiusnumber
borderColorstring
opacitynumber

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:

PropTypeNotes
sourcestringRequired — remote .json URL
widthnumberDefaults to "100%"
heightnumberDefaults to 200
autoPlaybooleanDefaults to true
loopbooleanDefaults to true
speednumberPlayback speed multiplier
opacitynumber
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidth / borderRadius / borderColornumber | stringApplied via a wrapping View

Dependencies: lottie-react-native

npx expo install lottie-react-native
tip

If lottie-react-native is not installed, the element renders a placeholder view with an install hint instead of crashing.

Rive props:

PropTypeNotes
urlstringRequired — remote .riv URL
widthnumberDefaults to "100%"
heightnumberOptional. When omitted, aspectRatio sizes the box
aspectRationumberUsed 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
autoplaybooleanDefaults to true
fit"Contain" | "Cover" | "Fill" | "FitWidth" | "FitHeight" | "None" | "ScaleDown" | "Layout"
alignment"TopLeft" | "TopCenter" | "TopRight" | "CenterLeft" | "Center" | "CenterRight" | "BottomLeft" | "BottomCenter" | "BottomRight"
artboardNamestringTarget artboard in the .riv file
stateMachineNamestringState machine to drive
opacitynumber
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidth / borderRadius / borderColornumber | stringApplied via a wrapping View

Dependencies: rive-react-native

npx expo install rive-react-native
tip

If rive-react-native is not installed, the element renders a placeholder view with an install hint instead of crashing.

Video props:

PropTypeNotes
urlstringRequired — remote video URL
widthnumberDefaults to "100%"
heightnumberDefaults to 200
autoPlaybooleanStart playing on mount; defaults to false
loopbooleanLoop playback; defaults to false
mutedbooleanMute audio; defaults to true (required for autoplay on iOS)
controlsbooleanShow native playback controls; defaults to false
opacitynumber
margin / marginHorizontal / marginVerticalnumber
padding / paddingHorizontal / paddingVerticalnumber
borderWidthnumberApplied via a wrapping View
borderRadiusnumberApplied via a wrapping View
borderColorstringApplied via a wrapping View

Dependencies: expo-video

npx expo install expo-video
tip

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" } }
}}
/>

Learn more about theming →

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