suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Page Indicator

A row of dots that shows the current page in a pager or carousel, with an elongated dot for the active page.

Page Indicator is an open-source component 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.

iOS
Page Indicator on iOS in light modePage Indicator on iOS in dark mode
import { PageIndicator } from "@/components/ui/page-indicator"

export function PageIndicatorExample() {
  return (
    <PageIndicator count={5} index={1} />
  )
}
import EsuissUI
import SwiftUI

struct PageIndicatorExample: View {
  var body: some View {
    PageIndicator(count: 5, index: 1)
  }
}
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.PageIndicator

@Composable
fun PageIndicatorExample() {
    PageIndicator(count = 5, index = 1)
}

PageIndicator draws one dot per page and widens the active one into a pill. Width and position changes animate with Reanimated. When count exceeds maxVisible, only a window of dots is shown; the window slides to keep the active dot near the center and the dots at its edges shrink to hint that more pages exist.

It comes in two sizes, default and sm, and two variants: default uses the foreground color for the active dot and accent uses the primary color.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/page-indicator

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add page-indicator
$ pnpm dlx @esuiss/ui@latest add page-indicator --platform swift
$ pnpm dlx @esuiss/ui@latest add page-indicator --platform kotlin

Manual

  1. Install the dependencies:
npx expo install cn react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: the primitives components/primitives/haptics.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

uniwind/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
  1. Copy the source into components/ui/page-indicator.tsx:
uniwind/components/ui/page-indicator.tsx
import { cn } from "cn"
import * as React from "react"
import {
  type AccessibilityActionEvent,
  Pressable,
  View,
  type ViewProps,
} from "react-native"
import {
  Easing,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { useHaptics } from "@/components/primitives/haptics"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: the primitives components/primitives/haptics.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

nativewind/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
  1. Copy the source into components/ui/page-indicator.tsx:
nativewind/components/ui/page-indicator.tsx
import { cn } from "cn"
import * as React from "react"
import {
  type AccessibilityActionEvent,
  Pressable,
  View,
  type ViewProps,
} from "react-native"
import {
  Easing,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { useHaptics } from "@/components/primitives/haptics"
  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: the primitives components/primitives/haptics.tsx, and the animated view components/ui/animated-view.tsx from the next step.

  2. Copy the animated view into components/ui/animated-view.tsx:

stylesheet/components/ui/animated-view.tsx
import { Pressable } from "react-native"
import Animated from "react-native-reanimated"

const AnimatedPressable = Animated.createAnimatedComponent(Pressable)

type AnimatedViewProps =
  | (React.ComponentProps<typeof Animated.View> &
      React.RefAttributes<React.ComponentRef<typeof Animated.View>> & {
        as?: "View"
      })
  | (React.ComponentProps<typeof AnimatedPressable> &
      React.RefAttributes<React.ComponentRef<typeof AnimatedPressable>> & {
        as: "Pressable"
      })

function AnimatedView(props: AnimatedViewProps) {
  1. Copy the source into components/ui/page-indicator.tsx:
stylesheet/components/ui/page-indicator.tsx
import * as React from "react"
import {
  type AccessibilityActionEvent,
  Pressable,
  StyleSheet,
  View,
  type ViewProps,
} from "react-native"
import {
  Easing,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from "react-native-reanimated"
import { useHaptics } from "@/components/primitives/haptics"
  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/PageIndicator.swift
import SwiftUI

public enum PageIndicatorSize: CaseIterable, Sendable {
  case sm, `default`

  var dot: CGFloat {
    switch self {
    case .sm: 6
    case .default: 8
    }
  }

  var activeWidth: CGFloat {
    switch self {
    case .sm: 16
    case .default: 24
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/Carousel.swift
import PhosphorSwift
import SwiftUI

public enum CarouselOrientation: CaseIterable, Sendable {
  case horizontal, vertical
}

public enum CarouselDotsVariant: CaseIterable, Sendable {
  case `default`, plain
}

let carouselAnimation = Animation.timingCurve(0.32, 0.72, 0, 1, duration: 0.5)

enum CarouselMath {
  static func clamp(_ index: Int, count: Int) -> Int {
    count == 0 ? 0 : min(max(index, 0), count - 1)
swift/Components/Haptics.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var hapticsEnabled = true
}

extension View {
  public func haptics(_ enabled: Bool) -> some View {
    environment(\.hapticsEnabled, enabled)
  }

  func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
    modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
  }

  func hapticFeedback<Trigger: Equatable>(
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
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")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/PageIndicator.kt
package com.esuiss.ui.components

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Carousel.kt
package com.esuiss.ui.components

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Haptics.kt
package com.esuiss.ui.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

val LocalHapticsEnabled = staticCompositionLocalOf { true }

@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { PageIndicator } from "@/components/ui/page-indicator"
<PageIndicator count={5} index={1} />

The component is controlled: pass count and the current index. Pass onIndexChange to make the dots pressable; without it the indicator is display-only. Pressing a dot plays a selection haptic.

The PageIndicatorSize and PageIndicatorVariant types are also exported.

Examples

Sizes

The sm and default sizes.

iOS
Page Indicator (sizes) on iOS in light modePage Indicator (sizes) on iOS in dark mode
import { View } from "react-native"
import { PageIndicator } from "@/components/ui/page-indicator"

export function PageIndicatorSizesExample() {
  return (
    <View className="items-center gap-6">
      <PageIndicator count={5} index={1} size="sm" />
      <PageIndicator count={5} index={1} />
    </View>
  )
}
import EsuissUI
import SwiftUI

struct PageIndicatorSizesExample: View {
  var body: some View {
    VStack(spacing: 24) {
      PageIndicator(count: 5, index: 1, size: .sm)
      PageIndicator(count: 5, index: 1)
    }
    .frame(maxWidth: .infinity)
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.PageIndicator
import com.esuiss.ui.components.PageIndicatorSize

@Composable
fun PageIndicatorCentered(spacing: Int, content: @Composable () -> Unit) {
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(spacing.dp),
    ) {
        content()
    }
}

@Composable
fun PageIndicatorSizes() {
    PageIndicatorCentered(24) {
        PageIndicator(count = 5, index = 1, size = PageIndicatorSize.Sm)
        PageIndicator(count = 5, index = 1)
    }
}

Accent

The accent variant, which colors the active dot with bg-primary.

iOS
Page Indicator (accent) on iOS in light modePage Indicator (accent) on iOS in dark mode
import { PageIndicator } from "@/components/ui/page-indicator"

export function PageIndicatorAccentExample() {
  return (
    <PageIndicator count={5} index={1} variant="accent" />
  )
}
import EsuissUI
import SwiftUI

struct PageIndicatorAccentExample: View {
  var body: some View {
    PageIndicator(count: 5, index: 1, variant: .accent)
  }
}
import androidx.compose.runtime.Composable
import com.esuiss.ui.components.PageIndicator
import com.esuiss.ui.components.PageIndicatorVariant

@Composable
fun PageIndicatorAccentExample() {
    PageIndicator(count = 5, index = 1, variant = PageIndicatorVariant.Accent)
}

Many pages

Twelve pages shown in a sliding window at the start, middle and end.

iOS
Page Indicator (many pages) on iOS in light modePage Indicator (many pages) on iOS in dark mode
import { View } from "react-native"
import { PageIndicator } from "@/components/ui/page-indicator"

export function PageIndicatorManyPagesExample() {
  return (
    <View className="items-center gap-6">
      <PageIndicator count={12} index={0} />
      <PageIndicator count={12} index={5} />
      <PageIndicator count={12} index={11} />
    </View>
  )
}
import EsuissUI
import SwiftUI

struct PageIndicatorManyPagesExample: View {
  var body: some View {
    VStack(spacing: 24) {
      PageIndicator(count: 12, index: 0)
      PageIndicator(count: 12, index: 5)
      PageIndicator(count: 12, index: 11)
    }
    .frame(maxWidth: .infinity)
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.PageIndicator

@Composable
fun PageIndicatorCentered(spacing: Int, content: @Composable () -> Unit) {
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(spacing.dp),
    ) {
        content()
    }
}

@Composable
fun PageIndicatorManyPages() {
    PageIndicatorCentered(24) {
        PageIndicator(count = 12, index = 0)
        PageIndicator(count = 12, index = 5)
        PageIndicator(count = 12, index = 11)
    }
}

Interactive

Pressable dots that change the page through onIndexChange.

iOS
Page Indicator (interactive) on iOS in light modePage Indicator (interactive) on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { PageIndicator } from "@/components/ui/page-indicator"
import { Text } from "@/components/ui/text"

function InteractiveIndicator({ count }: { count: number }) {
  const [index, setIndex] = React.useState(0)
  return (
    <View className="items-center gap-3">
      <PageIndicator count={count} index={index} onIndexChange={setIndex} />
      <Text variant="muted">
        Page {index + 1} of {count}
      </Text>
    </View>
  )
}

export function PageIndicatorInteractiveExample() {
  return (
    <InteractiveIndicator count={12} />
  )
}
import EsuissUI
import SwiftUI

struct PageIndicatorInteractiveExample: View {
  var body: some View {
    PageIndicatorInteractive(count: 12)
  }
}

struct PageIndicatorInteractive: View {
  let count: Int

  @State private var index = 0

  var body: some View {
    VStack(spacing: 12) {
      PageIndicator(count: count, index: index) { index = $0 }
      Text(verbatim: "Page \(index + 1) of \(count)")
        .textVariant(.muted)
    }
    .frame(maxWidth: .infinity)
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.PageIndicator
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun PageIndicatorInteractiveExample() {
    PageIndicatorInteractive(12)
}

@Composable
fun PageIndicatorCentered(spacing: Int, content: @Composable () -> Unit) {
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(spacing.dp),
    ) {
        content()
    }
}

@Composable
fun PageIndicatorInteractive(count: Int) {
    var index by rememberSaveable { mutableIntStateOf(0) }
    PageIndicatorCentered(12) {
        PageIndicator(count = count, index = index, onIndexChange = { index = it })
        Text("Page ${index + 1} of $count", variant = TextVariant.Muted)
    }
}

API Reference

01

PageIndicator

PropTypeDefaultDescription
countnumberRequiredTotal number of pages. Nothing renders when it is 0.
indexnumberRequiredZero-based index of the current page.
onIndexChange(index: number) => void-Called with the new index when a dot is pressed or an accessibility action is used.
sizePageIndicatorSize"default"Dot size: default or sm.
variantPageIndicatorVariant"default"Active dot color: default or accent.
maxVisiblenumber7Most dots shown at once. Defaults to 7, minimum 3.

Also accepts the props of ViewProps.

Accessibility

  • The indicator is a single element with the adjustable role, and the dots are not focusable on their own.
  • Its value is announced as "2 of 5".
  • When onIndexChange is set, VoiceOver and TalkBack users swipe up or down to move to the next or previous page.
  • Animations respect the system reduce motion setting.

Notes

  • Uses bg-foreground or bg-primary for the active dot and bg-border for the others; colors follow the system appearance.
NextPaginationPreviousNavigation Menu

On this page

  1. 01Installation
    • Manual
  2. 02Usage
  3. 03Examples
    • Sizes
    • Accent
    • Many pages
    • Interactive
  4. 04API Reference
    • PageIndicator
  5. 05Accessibility
  6. 06Notes