Components PulseCounter
PulseCounter
Numbers that count up.
interactive demo
0
rolls to 12,890 with a springA live browser stand-in using the exact tokens, motion and API of the SwiftUI component.
Want all components at once?
Open the studio
Numbers that count up.
An animated ticker for stats and dashboards. Spins through digits on value change — great for revenue, users and scores.
Digit-roll animation
Arbitrary fonts
String or number values
Reduced-motion safe
Usage
PulseCounter(value: 1289, font: .system(size: 48, weight: .bold)) Install
$ pulse add counter Source files
Sources/PulseUI/Components/PulseCounter/PulseCounter.swift
Full source Sources/PulseUI/Components/PulseCounter/PulseCounter.swift
import SwiftUI
// MARK: - PulseCounter
/// Animated counter with eased number transitions and configurable formatting.
public struct PulseCounter: View {
@Environment(\.pulseTheme) private var theme
@Environment(\.accessibilityReduceMotion) private var reduceMotion
let value: Int
let font: Font
let fontWeight: Font.Weight
let foreground: Color?
let dampingFraction: Double
let format: (Int) -> String
@State private var displayValue: Double = 0
public init(
value: Int,
font: Font = .system(size: 44, weight: .bold, design: .rounded),
fontWeight: Font.Weight = .bold,
foreground: Color? = nil,
dampingFraction: Double = 0.8,
format: @escaping (Int) -> String = { "\($0)" }
) {
self.value = value
self.font = font
self.fontWeight = fontWeight
self.foreground = foreground
self.dampingFraction = dampingFraction
self.format = format
}
public var body: some View {
Text(format(Int(displayValue)))
.font(font)
.fontWeight(fontWeight)
.foregroundStyle(foreground ?? theme.colors.foreground)
.monospacedDigit()
.contentTransition(.numericText())
.animation(reduceMotion ? .none : .spring(response: 0.5, dampingFraction: 0.8), value: displayValue)
.onAppear {
displayValue = Double(value)
}
.onChange(of: value) { _, newValue in
guard !reduceMotion else {
displayValue = Double(newValue)
return
}
withAnimation(.spring(response: 0.4, dampingFraction: dampingFraction)) {
displayValue = Double(newValue)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(format(value))
}
}
// MARK: - Money Counter
public struct PulseMoneyCounter: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
let cents: Int
let currencyCode: String
let font: Font
let fontWeight: Font.Weight
@State private var displayCents: Int = 0
public init(
cents: Int,
currencyCode: String = "USD",
font: Font = .system(size: 44, weight: .bold, design: .rounded),
fontWeight: Font.Weight = .bold
) {
self.cents = cents
self.currencyCode = currencyCode
self.font = font
self.fontWeight = fontWeight
}
public var body: some View {
Text(formatted)
.font(font)
.fontWeight(fontWeight)
.monospacedDigit()
.contentTransition(.numericText())
.animation(reduceMotion ? .none : .spring(response: 0.5, dampingFraction: 0.85), value: displayCents)
.onAppear {
displayCents = cents
}
.onChange(of: cents) { _, newValue in
guard !reduceMotion else {
displayCents = newValue
return
}
withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) {
displayCents = newValue
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(formatted)
}
private var formatted: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencyCode = currencyCode
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
return formatter.string(from: NSNumber(value: Double(displayCents) / 100)) ?? "$0.00"
}
}