SwiftUI Page Indicator Component
Page dots where the active one stretches into a capsule instead of just changing colour. Tappable, so it doubles as navigation.
Page 1
Variants: Dots, Bars
Install the free SwiftUI page indicator
- Add SwiftKitUI.swift to your app once. It holds the tokens and motion curves.
- Copy SKPageIndicator.swift below into your project. It needs nothing else.
Usage
Usage.swift
SKPageIndicator(count: 4, index: $page)
SKPageIndicator(count: 4, index: $page, style: .bars)Full source · 102 lines · MIT
SKPageIndicator.swift
//
// SKPageIndicator.swift
// Swift Kit UI
//
// Requires: SwiftKitUI.swift
//
// Page dots where the active one stretches into a capsule instead of just
// changing colour. Tappable, so it doubles as navigation.
//
// Usage:
// SKPageIndicator(count: 4, index: $page)
// SKPageIndicator(count: 4, index: $page, style: .bars)
//
import SwiftUI
public struct SKPageIndicator: View {
public enum Style: Sendable { case dots, bars }
private let count: Int
@Binding private var index: Int
private let style: Style
private let tint: Color
@Environment(\.accessibilityReduceMotion) private var reduceMotion
public init(
count: Int,
index: Binding<Int>,
style: Style = .dots,
tint: Color = SK.Palette.accent
) {
self.count = count
self._index = index
self.style = style
self.tint = tint
}
public var body: some View {
HStack(spacing: style == .dots ? SK.Space.sm : SK.Space.xs) {
ForEach(0..<max(count, 1), id: \.self) { page in
dot(page)
}
}
.accessibilityElement()
.accessibilityLabel("Page")
.accessibilityValue("\(index + 1) of \(count)")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: if index + 1 < count { index += 1 }
case .decrement: if index > 0 { index -= 1 }
@unknown default: break
}
}
}
private func dot(_ page: Int) -> some View {
let active = page == index
let height: CGFloat = style == .dots ? 8 : 4
let activeWidth: CGFloat = style == .dots ? 24 : 28
let restWidth: CGFloat = style == .dots ? 8 : 14
return Capsule()
.fill(active ? tint : SK.Palette.separator)
.frame(width: active ? activeWidth : restWidth, height: height)
// Hit target stays a comfortable size even though the dot is tiny.
.frame(width: max(activeWidth, 30), height: SK.Space.xxl, alignment: .center)
.contentShape(.rect)
.onTapGesture {
guard !active else { return }
withAnimation(SKMotion.snap.resolved(reduceMotion: reduceMotion)) { index = page }
SK.Haptic.play(.selection)
}
.skAnimation(.snap, value: index)
}
}
#Preview("SKPageIndicator") {
@Previewable @State var page = 0
VStack(spacing: SK.Space.xxl) {
TabView(selection: $page) {
ForEach(0..<4, id: \.self) { i in
RoundedRectangle(cornerRadius: SK.Radius.xl, style: .continuous)
.fill(SK.Palette.accentSoft)
.overlay(Text("Page \(i + 1)").font(SK.Type_.title))
.padding(SK.Space.lg)
.tag(i)
}
}
#if os(iOS)
.tabViewStyle(.page(indexDisplayMode: .never))
#endif
.frame(height: 220)
SKPageIndicator(count: 4, index: $page)
SKPageIndicator(count: 4, index: $page, style: .bars, tint: SK.Palette.success)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(SK.Palette.surfaceSunken)
}
Questions
- Is this SwiftUI page indicator free for commercial apps?
- Yes. Swift Kit UI is MIT licensed, so you can use it in personal and commercial apps with no payment or sign-up. Keep the licence notice with the code.
- What does it depend on?
- SKPageIndicator.swift depends only on SwiftKitUI.swift, a single foundation file with the design tokens and motion curves. There are no third-party packages.
- Which iOS versions does it support?
- iOS 17 and later, plus macOS 14.
Related components
Building a whole app? The Swift Kit adds auth, payments and onboarding, ready to ship.