AniUI
Blocks/Flow

Onboarding

A welcome carousel with step indicators, slide content, and a skip option. Drop it in before your main navigator.

Welcome

Discover a new way to manage your tasks and boost productivity.

Installation

npx @aniui/cli add text button

Also requires react-native-safe-area-context for SafeAreaView.

Usage

Render this screen before your main app navigator. Call onFinish to push users into the app and persist a flag so it only shows once.

import { OnboardingScreen } from "@/screens/onboarding";

// In your root navigator:
<Stack.Screen name="Onboarding" component={OnboardingScreen} />

Source

screens/onboarding.tsx
import React, { useState } from "react";
import { View, Pressable } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Text } from "@/components/ui/text";
import { Button } from "@/components/ui/button";

const steps = [
  {
    title: "Welcome",
    description: "Discover a new way to manage your tasks and boost productivity.",
    icon: (
      <Svg width={64} height={64} viewBox="0 0 64 64" fill="none">
        <Path
          d="M32 4C32 4 20 16 20 32c0 6.627 5.373 12 12 12s12-5.373 12-12C44 16 32 4 32 4z"
          stroke="hsl(var(--primary))" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
        />
        <Path d="M20 40l-8 8M44 40l8 8" stroke="hsl(var(--primary))" strokeWidth="2.5" strokeLinecap="round" />
        <Circle cx="32" cy="32" r="4" fill="hsl(var(--primary))" />
      </Svg>
    ),
  },
  {
    title: "Stay Focused",
    description: "Set goals, track progress, and achieve more every day.",
    icon: (
      <Svg width={64} height={64} viewBox="0 0 64 64" fill="none">
        <Circle cx="32" cy="32" r="28" stroke="hsl(var(--primary))" strokeWidth="2.5" />
        <Circle cx="32" cy="32" r="18" stroke="hsl(var(--primary))" strokeWidth="2.5" />
        <Circle cx="32" cy="32" r="8" stroke="hsl(var(--primary))" strokeWidth="2.5" />
        <Circle cx="32" cy="32" r="3" fill="hsl(var(--primary))" />
        <Path d="M32 4v8M32 52v8M4 32h8M52 32h8" stroke="hsl(var(--primary))" strokeWidth="2.5" strokeLinecap="round" />
      </Svg>
    ),
  },
  {
    title: "Connect",
    description: "Collaborate with your team and share updates in real-time.",
    icon: (
      <Svg width={64} height={64} viewBox="0 0 64 64" fill="none">
        <Circle cx="20" cy="24" r="8" stroke="hsl(var(--primary))" strokeWidth="2.5" />
        <Circle cx="44" cy="24" r="8" stroke="hsl(var(--primary))" strokeWidth="2.5" />
        <Path d="M4 52c0-8.837 7.163-16 16-16h4" stroke="hsl(var(--primary))" strokeWidth="2.5" strokeLinecap="round" />
        <Path d="M60 52c0-8.837-7.163-16-16-16h-4" stroke="hsl(var(--primary))" strokeWidth="2.5" strokeLinecap="round" />
        <Path d="M24 36h16" stroke="hsl(var(--primary))" strokeWidth="2.5" strokeLinecap="round" />
      </Svg>
    ),
  },
];

export function OnboardingScreen({ onFinish }: { onFinish?: () => void }) {
  const [step, setStep] = useState(0);
  const isLast = step === steps.length - 1;
  const current = steps[step];

  return (
    <SafeAreaView className="flex-1 bg-background">
      <View className="flex-1 justify-between px-6 py-8">
        {/* Skip */}
        <View className="items-end">
          <Pressable
            onPress={() => setStep(steps.length - 1)}
            accessible={true}
            accessibilityRole="button"
          >
            <Text className="text-sm text-muted-foreground">Skip</Text>
          </Pressable>
        </View>

        {/* Slide content */}
        <View className="flex-1 items-center justify-center gap-8">
          <View className="w-24 h-24 rounded-full bg-primary/10 items-center justify-center">
            {current.icon}
          </View>
          <View className="items-center gap-3">
            <Text variant="h2" className="text-center">{current.title}</Text>
            <Text variant="muted" className="text-center px-4">
              {current.description}
            </Text>
          </View>
        </View>

        {/* Dots */}
        <View className="flex-row justify-center gap-2 mb-6">
          {steps.map((_, i) => (
            <View
              key={i}
              className={`h-2 rounded-full ${
                i === step ? "w-8 bg-primary" : "w-2 bg-primary/20"
              }`}
            />
          ))}
        </View>

        {/* Next / Get Started */}
        <Button
          className="h-12 rounded-xl"
          onPress={() => {
            if (isLast) {
              onFinish?.();
            } else {
              setStep((s) => s + 1);
            }
          }}
        >
          {isLast ? "Get Started" : "Next"}
        </Button>
      </View>
    </SafeAreaView>
  );
}