# Onboarding

A paged onboarding flow with illustrations, a page indicator and Continue and Skip buttons.

Onboarding is an open-source block from suiss/ui for React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/blocks/onboarding

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/blocks-onboarding--default.light.webp)

```tsx title="uniwind/components/blocks/onboarding.tsx"
import { cn } from "cn"
import {
  BellRingingIcon,
  CloudIcon,
  type Icon as PhosphorIcon,
  ShieldCheckIcon,
  SparkleIcon,
} from "phosphor-react-native"
import type * as React from "react"
import { View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  useCarousel,
} from "@/components/ui/carousel"
import { Icon } from "@/components/ui/icon"
import { PageIndicator } from "@/components/ui/page-indicator"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

type OnboardingPage = {
  title: string
  description: string
  illustration?: React.ReactNode
}

function Illustration({ icon }: { icon: PhosphorIcon }) {
  return (
    <View className="size-40 items-center justify-center rounded-full bg-primary/10 dark:bg-primary/20">
      <Icon as={icon} weight="duotone" className="size-20 text-primary" />
    </View>
  )
}

const DEFAULT_PAGES: OnboardingPage[] = [
  {
    title: "Welcome to suiss",
    description: "Everything you need, together in one place.",
    illustration: <Illustration icon={SparkleIcon} />,
  },
  {
    title: "Always in sync",
    description:
      "Your photos, files and passwords stay up to date on every device.",
    illustration: <Illustration icon={CloudIcon} />,
  },
  {
    title: "Private by design",
    description: "Your data is end-to-end encrypted. Only you can read it.",
    illustration: <Illustration icon={ShieldCheckIcon} />,
  },
  {
    title: "Never miss a thing",
    description: "Choose what matters and we'll let you know when it happens.",
    illustration: <Illustration icon={BellRingingIcon} />,
  },
]

function OnboardingFooter({
  count,
  onDone,
}: {
  count: number
  onDone?: () => void
}) {
  const { selectedIndex, scrollNext, scrollTo } = useCarousel()
  const last = selectedIndex >= count - 1

  return (
    <BottomBar>
      <PageIndicator
        count={count}
        index={selectedIndex}
        onIndexChange={(index) => scrollTo(index)}
        className="mb-2"
      />
      <BottomBarContent>
        <Button
          size="lg"
          block
          onPress={() => (last ? onDone?.() : scrollNext())}
        >
          <Text>{last ? "Get started" : "Continue"}</Text>
        </Button>
        <Button
          size="lg"
          variant="plain"
          block
          disabled={last}
          accessibilityElementsHidden={last}
          importantForAccessibility={last ? "no-hide-descendants" : "auto"}
          onPress={onDone}
          className={cn(last && "opacity-0")}
        >
          <Text>Skip</Text>
        </Button>
      </BottomBarContent>
    </BottomBar>
  )
}

function Onboarding({
  pages = DEFAULT_PAGES,
  onDone,
}: {
  pages?: OnboardingPage[]
  onDone?: () => void
}) {
  return (
    <Screen>
      <Carousel className="flex-1">
        <View className="flex-1 justify-center">
          <CarouselContent>
            {pages.map((page) => (
              <CarouselItem key={page.title}>
                <View className="items-center gap-4 px-8 py-6">
                  {page.illustration ? (
                    <View
                      accessible={false}
                      importantForAccessibility="no-hide-descendants"
                      className="mb-6"
                    >
                      {page.illustration}
                    </View>
                  ) : null}
                  <Text
                    role="heading"
                    className="text-center text-3xl font-bold"
                  >
                    {page.title}
                  </Text>
                  <Text className="text-center text-lg text-muted-foreground">
                    {page.description}
                  </Text>
                </View>
              </CarouselItem>
            ))}
          </CarouselContent>
        </View>
        <OnboardingFooter count={pages.length} onDone={onDone} />
      </Carousel>
    </Screen>
  )
}

export type { OnboardingPage }
export { Onboarding }
```

```swift title="swift/Blocks/Onboarding.swift"
import PhosphorSwift
import SwiftUI

public struct OnboardingPage: Identifiable {
  public let id = UUID()
  let title: Text
  let description: Text
  let illustration: AnyView?

  public init<Illustration: View>(
    _ title: LocalizedStringKey,
    description: LocalizedStringKey,
    @ViewBuilder illustration: () -> Illustration
  ) {
    self.title = Text(title)
    self.description = Text(description)
    self.illustration = AnyView(illustration())
  }

  public init<Illustration: View>(
    verbatim title: String,
    description: String,
    @ViewBuilder illustration: () -> Illustration
  ) {
    self.title = Text(verbatim: title)
    self.description = Text(verbatim: description)
    self.illustration = AnyView(illustration())
  }

  public init(_ title: LocalizedStringKey, description: LocalizedStringKey) {
    self.title = Text(title)
    self.description = Text(description)
    illustration = nil
  }

  public init(verbatim title: String, description: String) {
    self.title = Text(verbatim: title)
    self.description = Text(verbatim: description)
    illustration = nil
  }

  public static var samples: [OnboardingPage] {
    [
      OnboardingPage("Everything in one place", description: "Your accounts, cards and savings together, always up to date.") {
        OnboardingSymbol(icon: Ph.squaresFour.duotone)
      },
      OnboardingPage("Pay in a tap", description: "Send money to friends and pay in stores with a single tap.") {
        OnboardingSymbol(icon: Ph.contactlessPayment.duotone)
      },
      OnboardingPage("Stay in control", description: "Get a notification for every payment and freeze your card any time.") {
        OnboardingSymbol(icon: Ph.bellRinging.duotone)
      },
      OnboardingPage("Safe by design", description: "Face ID, passkeys and end-to-end encryption keep your money yours.") {
        OnboardingSymbol(icon: Ph.shieldCheck.duotone)
      },
    ]
  }
}

private struct OnboardingSymbol: View {
  let icon: Image

  var body: some View {
    icon
      .frame(width: 96, height: 96)
      .foregroundStyle(EsuissColor.primary)
      .frame(width: 200, height: 200)
      .background(Color.adaptive(light: EsuissColor.primary.opacity(0.1), dark: EsuissColor.primary.opacity(0.2)), in: Circle())
  }
}

public struct Onboarding: View {
  let pages: [OnboardingPage]
  let skipLabel: LocalizedStringKey
  let continueLabel: LocalizedStringKey
  let doneLabel: LocalizedStringKey
  let onDone: () -> Void

  @State private var controller = CarouselController()
  private var initialIndex = 0

  public init(
    pages: [OnboardingPage] = OnboardingPage.samples,
    skipLabel: LocalizedStringKey = "Skip",
    continueLabel: LocalizedStringKey = "Continue",
    doneLabel: LocalizedStringKey = "Get started",
    onDone: @escaping () -> Void
  ) {
    self.pages = pages
    self.skipLabel = skipLabel
    self.continueLabel = continueLabel
    self.doneLabel = doneLabel
    self.onDone = onDone
  }

  init(pages: [OnboardingPage], index: Int, onDone: @escaping () -> Void) {
    self.init(pages: pages, onDone: onDone)
    initialIndex = index
  }

  public var body: some View {
    let last = controller.selectedIndex >= pages.count - 1
    Screen {
      Carousel(controller: controller) {
        CarouselContent(spacing: 0) {
          ForEach(pages) { page in
            CarouselItem {
              OnboardingSlide(page: page)
            }
          }
        }
      }
      .frame(maxHeight: .infinity)
      .padding(.top, 24)
      PageIndicator(count: pages.count, index: controller.selectedIndex) { index in
        controller.scrollTo(index)
      }
      .padding(.bottom, 8)
      BottomBar(separator: false) {
        BottomBarContent {
          Button(last ? doneLabel : continueLabel) {
            if last {
              onDone()
            } else {
              controller.scrollNext()
            }
          }
          .buttonStyle(.variant(.default, size: .lg, block: true))
          .contentTransition(.opacity)
          Button(skipLabel, action: onDone)
            .buttonStyle(.variant(.plain, size: .lg, block: true))
            .opacity(last ? 0 : 1)
            .disabled(last)
            .accessibilityHidden(last)
        }
      }
    }
    .task {
      guard initialIndex > 0 else { return }
      try? await Task.sleep(for: .milliseconds(100))
      controller.scrollTo(initialIndex, animated: false)
    }
  }
}

private struct OnboardingSlide: View {
  let page: OnboardingPage

  var body: some View {
    VStack(spacing: 0) {
      Group {
        if let illustration = page.illustration {
          illustration
        } else {
          Color.clear
        }
      }
      .frame(maxWidth: .infinity, maxHeight: .infinity)
      .accessibilityHidden(true)
      VStack(spacing: 12) {
        page.title
          .scaledTextStyle(.xl2, weight: .bold)
          .foregroundStyle(EsuissColor.foreground)
          .accessibilityAddTraits(.isHeader)
        page.description
          .scaledTextStyle(.lg)
          .foregroundStyle(EsuissColor.mutedForeground)
      }
      .multilineTextAlignment(.center)
      .fixedSize(horizontal: false, vertical: true)
      .padding(.horizontal, 32)
      .padding(.bottom, 24)
    }
    .accessibilityElement(children: .combine)
  }
}
```

```kt title="kotlin/blocks/Onboarding.kt"
package com.esuiss.ui.blocks

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.BellRinging
import com.adamglin.phosphoricons.regular.CloudCheck
import com.adamglin.phosphoricons.regular.ShieldCheck
import com.adamglin.phosphoricons.regular.Sparkle
import com.esuiss.ui.components.BottomBar
import com.esuiss.ui.components.BottomBarContent
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Carousel
import com.esuiss.ui.components.CarouselContent
import com.esuiss.ui.components.CarouselItem
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.PageIndicator
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.rememberCarouselState
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Immutable
class OnboardingPage(
    val title: String,
    val description: String,
    val illustration: (@Composable () -> Unit)? = null,
)

private fun iconPage(title: String, description: String, icon: ImageVector) = OnboardingPage(title, description) {
    val colors = EsuissTheme.colors
    Box(
        Modifier
            .size(200.dp)
            .background(colors.primary.copy(alpha = if (colors.isDark) 0.2f else 0.1f), CircleShape),
        contentAlignment = Alignment.Center,
    ) {
        Icon(icon, contentDescription = null, size = 96.dp, tint = colors.primary)
    }
}

private val DemoPages = listOf(
    iconPage("Welcome to suiss", "Everything you love, together in one place.", PhosphorIcons.Regular.Sparkle),
    iconPage("Always in sync", "Your photos, notes and passwords stay up to date on every device.", PhosphorIcons.Regular.CloudCheck),
    iconPage("Private by design", "Your data is encrypted end to end. Only you can read it.", PhosphorIcons.Regular.ShieldCheck),
    iconPage("Stay in the loop", "Get notified about the things that matter to you.", PhosphorIcons.Regular.BellRinging),
)

@Composable
fun Onboarding(
    modifier: Modifier = Modifier,
    pages: List<OnboardingPage> = DemoPages,
    onDone: () -> Unit = {},
) {
    val colors = EsuissTheme.colors
    val state = rememberCarouselState(pageCount = pages.size)
    val last = state.selectedIndex >= pages.lastIndex
    Screen(modifier) {
        Column(Modifier.fillMaxSize()) {
            Carousel(state = state, modifier = Modifier.weight(1f)) {
                CarouselContent(modifier = Modifier.weight(1f), pageSpacing = 0.dp) { index ->
                    val page = pages[index]
                    CarouselItem(Modifier.fillMaxSize()) {
                        Column(
                            modifier = Modifier
                                .fillMaxSize()
                                .padding(horizontal = 32.dp),
                            verticalArrangement = Arrangement.Center,
                            horizontalAlignment = Alignment.CenterHorizontally,
                        ) {
                            val illustration = page.illustration
                            if (illustration != null) {
                                Box(Modifier.clearAndSetSemantics {}) { illustration() }
                                Spacer(Modifier.height(40.dp))
                            }
                            BasicText(
                                page.title,
                                modifier = Modifier.semantics { heading() },
                                style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center),
                            )
                            Spacer(Modifier.height(12.dp))
                            BasicText(
                                page.description,
                                modifier = Modifier.widthIn(max = 360.dp),
                                style = Typography.lg.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                            )
                        }
                    }
                }
            }
            PageIndicator(
                count = pages.size,
                index = state.selectedIndex,
                onIndexChange = { state.scrollTo(it) },
                modifier = Modifier
                    .align(Alignment.CenterHorizontally)
                    .padding(vertical = 12.dp),
            )
            BottomBar {
                BottomBarContent {
                    Button(
                        if (last) "Get started" else "Continue",
                        { if (last) onDone() else state.scrollNext() },
                        size = ButtonSize.Lg,
                        block = true,
                    )
                    Button(
                        "Skip",
                        onDone,
                        modifier = Modifier
                            .fillMaxWidth()
                            .graphicsLayer { alpha = if (last) 0f else 1f },
                        variant = ButtonVariant.Plain,
                        enabled = !last,
                    )
                }
            }
        }
    }
}
```

A `Carousel` of pages, each with an icon illustration, a title and a description. A `BottomBar` holds a `PageIndicator` you can tap to jump to a page, a Continue `Button` that moves to the next page and becomes Get started on the last, and a Skip button.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/onboarding
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add onboarding
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add onboarding --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add onboarding --platform kotlin
```

The CLI also adds what it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Carousel](https://ui.suiss.app/docs/components/carousel), [Icon](https://ui.suiss.app/docs/components/icon), [Page Indicator](https://ui.suiss.app/docs/components/page-indicator), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text).

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Carousel](https://ui.suiss.app/docs/components/carousel), [Icon](https://ui.suiss.app/docs/components/icon), [Page Indicator](https://ui.suiss.app/docs/components/page-indicator), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/onboarding.tsx`:

- `uniwind/components/blocks/onboarding.tsx`: shown above

4. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Carousel](https://ui.suiss.app/docs/components/carousel), [Icon](https://ui.suiss.app/docs/components/icon), [Page Indicator](https://ui.suiss.app/docs/components/page-indicator), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/onboarding.tsx`:

```tsx title="nativewind/components/blocks/onboarding.tsx"
import { cn } from "cn"
import {
  BellRingingIcon,
  CloudIcon,
  type Icon as PhosphorIcon,
  ShieldCheckIcon,
  SparkleIcon,
} from "phosphor-react-native"
import type * as React from "react"
import { View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  useCarousel,
} from "@/components/ui/carousel"
import { Icon } from "@/components/ui/icon"
import { PageIndicator } from "@/components/ui/page-indicator"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

type OnboardingPage = {
  title: string
  description: string
  illustration?: React.ReactNode
}

function Illustration({ icon }: { icon: PhosphorIcon }) {
  return (
    <View className="size-40 items-center justify-center rounded-full bg-primary-10 dark:bg-primary-20">
      <Icon as={icon} weight="duotone" className="size-20 text-primary" />
    </View>
  )
}

const DEFAULT_PAGES: OnboardingPage[] = [
  {
    title: "Welcome to suiss",
    description: "Everything you need, together in one place.",
    illustration: <Illustration icon={SparkleIcon} />,
  },
  {
    title: "Always in sync",
    description:
      "Your photos, files and passwords stay up to date on every device.",
    illustration: <Illustration icon={CloudIcon} />,
  },
  {
    title: "Private by design",
    description: "Your data is end-to-end encrypted. Only you can read it.",
    illustration: <Illustration icon={ShieldCheckIcon} />,
  },
  {
    title: "Never miss a thing",
    description: "Choose what matters and we'll let you know when it happens.",
    illustration: <Illustration icon={BellRingingIcon} />,
  },
]

function OnboardingFooter({
  count,
  onDone,
}: {
  count: number
  onDone?: () => void
}) {
  const { selectedIndex, scrollNext, scrollTo } = useCarousel()
  const last = selectedIndex >= count - 1

  return (
    <BottomBar>
      <PageIndicator
        count={count}
        index={selectedIndex}
        onIndexChange={(index) => scrollTo(index)}
        className="mb-2"
      />
      <BottomBarContent>
        <Button
          size="lg"
          block
          onPress={() => (last ? onDone?.() : scrollNext())}
        >
          <Text>{last ? "Get started" : "Continue"}</Text>
        </Button>
        <Button
          size="lg"
          variant="plain"
          block
          disabled={last}
          accessibilityElementsHidden={last}
          importantForAccessibility={last ? "no-hide-descendants" : "auto"}
          onPress={onDone}
          className={cn(last && "opacity-0")}
        >
          <Text>Skip</Text>
        </Button>
      </BottomBarContent>
    </BottomBar>
  )
}

function Onboarding({
  pages = DEFAULT_PAGES,
  onDone,
}: {
  pages?: OnboardingPage[]
  onDone?: () => void
}) {
  return (
    <Screen>
      <Carousel className="flex-1">
        <View className="flex-1 justify-center">
          <CarouselContent>
            {pages.map((page) => (
              <CarouselItem key={page.title}>
                <View className="items-center gap-4 px-8 py-6">
                  {page.illustration ? (
                    <View
                      accessible={false}
                      importantForAccessibility="no-hide-descendants"
                      className="mb-6"
                    >
                      {page.illustration}
                    </View>
                  ) : null}
                  <Text
                    role="heading"
                    className="text-center text-3xl font-bold"
                  >
                    {page.title}
                  </Text>
                  <Text className="text-center text-lg text-muted-foreground">
                    {page.description}
                  </Text>
                </View>
              </CarouselItem>
            ))}
          </CarouselContent>
        </View>
        <OnboardingFooter count={pages.length} onDone={onDone} />
      </Carousel>
    </Screen>
  )
}

export type { OnboardingPage }
export { Onboarding }
```

4. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-svg
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Carousel](https://ui.suiss.app/docs/components/carousel), [Icon](https://ui.suiss.app/docs/components/icon), [Page Indicator](https://ui.suiss.app/docs/components/page-indicator), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text).

3. Copy the source into `components/blocks/onboarding.tsx`:

```tsx title="stylesheet/components/blocks/onboarding.tsx"
import {
  BellRingingIcon,
  CloudIcon,
  type Icon as PhosphorIcon,
  ShieldCheckIcon,
  SparkleIcon,
} from "phosphor-react-native"
import type * as React from "react"
import { StyleSheet, View } from "react-native"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import {
  Carousel,
  CarouselContent,
  CarouselItem,
  useCarousel,
} from "@/components/ui/carousel"
import { Icon } from "@/components/ui/icon"
import { PageIndicator } from "@/components/ui/page-indicator"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
import { fontSize, radius, useTheme } from "@/components/ui/theme"

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

type OnboardingPage = {
  title: string
  description: string
  illustration?: React.ReactNode
}

const styles = StyleSheet.create({
  illustration: {
    width: 160,
    height: 160,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.full,
  },
  carousel: { flex: 1 },
  stage: { flex: 1, justifyContent: "center" },
  page: {
    alignItems: "center",
    gap: 16,
    paddingHorizontal: 32,
    paddingVertical: 24,
  },
  art: { marginBottom: 24 },
  title: { ...fontSize["3xl"], fontWeight: "700", textAlign: "center" },
  description: { ...fontSize.lg, textAlign: "center" },
  indicator: { marginBottom: 8 },
  hidden: { opacity: 0 },
})

function Illustration({ icon }: { icon: PhosphorIcon }) {
  const { colors, isDark } = useTheme()
  return (
    <View
      style={[
        styles.illustration,
        { backgroundColor: withAlpha(colors.primary, isDark ? 0.2 : 0.1) },
      ]}
    >
      <Icon as={icon} weight="duotone" size={80} color={colors.primary} />
    </View>
  )
}

const DEFAULT_PAGES: OnboardingPage[] = [
  {
    title: "Welcome to suiss",
    description: "Everything you need, together in one place.",
    illustration: <Illustration icon={SparkleIcon} />,
  },
  {
    title: "Always in sync",
    description:
      "Your photos, files and passwords stay up to date on every device.",
    illustration: <Illustration icon={CloudIcon} />,
  },
  {
    title: "Private by design",
    description: "Your data is end-to-end encrypted. Only you can read it.",
    illustration: <Illustration icon={ShieldCheckIcon} />,
  },
  {
    title: "Never miss a thing",
    description: "Choose what matters and we'll let you know when it happens.",
    illustration: <Illustration icon={BellRingingIcon} />,
  },
]

function OnboardingFooter({
  count,
  onDone,
}: {
  count: number
  onDone?: () => void
}) {
  const { selectedIndex, scrollNext, scrollTo } = useCarousel()
  const last = selectedIndex >= count - 1

  return (
    <BottomBar>
      <PageIndicator
        count={count}
        index={selectedIndex}
        onIndexChange={(index) => scrollTo(index)}
        style={styles.indicator}
      />
      <BottomBarContent>
        <Button
          size="lg"
          block
          onPress={() => (last ? onDone?.() : scrollNext())}
        >
          <Text>{last ? "Get started" : "Continue"}</Text>
        </Button>
        <Button
          size="lg"
          variant="plain"
          block
          disabled={last}
          accessibilityElementsHidden={last}
          importantForAccessibility={last ? "no-hide-descendants" : "auto"}
          onPress={onDone}
          style={last ? styles.hidden : undefined}
        >
          <Text>Skip</Text>
        </Button>
      </BottomBarContent>
    </BottomBar>
  )
}

function Onboarding({
  pages = DEFAULT_PAGES,
  onDone,
}: {
  pages?: OnboardingPage[]
  onDone?: () => void
}) {
  const { colors } = useTheme()
  return (
    <Screen>
      <Carousel style={styles.carousel}>
        <View style={styles.stage}>
          <CarouselContent>
            {pages.map((page) => (
              <CarouselItem key={page.title}>
                <View style={styles.page}>
                  {page.illustration ? (
                    <View
                      accessible={false}
                      importantForAccessibility="no-hide-descendants"
                      style={styles.art}
                    >
                      {page.illustration}
                    </View>
                  ) : null}
                  <Text role="heading" style={styles.title}>
                    {page.title}
                  </Text>
                  <Text
                    style={[
                      styles.description,
                      { color: colors.mutedForeground },
                    ]}
                  >
                    {page.description}
                  </Text>
                </View>
              </CarouselItem>
            ))}
          </CarouselContent>
        </View>
        <OnboardingFooter count={pages.length} onDone={onDone} />
      </Carousel>
    </Screen>
  )
}

export type { OnboardingPage }
export { Onboarding }
```

4. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

- `swift/Blocks/Onboarding.swift`: shown above

- `swift/Components/BottomBar.swift`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Carousel.swift`: see [Carousel](https://ui.suiss.app/docs/components/carousel.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

- `swift/Components/PageIndicator.swift`: see [Page Indicator](https://ui.suiss.app/docs/components/page-indicator.md)

- `swift/Components/Screen.swift`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/blocks/Onboarding.kt`: shown above

- `kotlin/components/BottomBar.kt`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Carousel.kt`: see [Carousel](https://ui.suiss.app/docs/components/carousel.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/PageIndicator.kt`: see [Page Indicator](https://ui.suiss.app/docs/components/page-indicator.md)

- `kotlin/components/Screen.kt`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { Onboarding } from "@/components/blocks/onboarding"
```

```tsx
<Onboarding />
```

Pass your own `pages` (each with a `title`, `description` and optional `illustration`), or edit `DEFAULT_PAGES`. `onDone` is called from Get started and Skip.

