
Rotating Text
From syxUI — written for both platforms, not translated between them.
A fixed lead-in with one word rotating vertically in a clipped window.
Text Effects · SwiftUI · Flutter · iOS 17 · Flutter 3.16
- text
- rotating
- carousel
- headline
- words
- animation
The actual source
RotatingText.swift
// Rotating Text · syxUI · https://syxui.dev/components/rotating-text
// Free in any project. Keep this line and credit syxUI where a person can read it.
import SwiftUI
/// A fixed lead-in with one word rotating vertically beneath a clipped window.
///
/// The window is sized to the tallest word, so the line never reflows as words
/// change — the single detail that separates this from a jittery mess.
struct RotatingText: View {
var prefix: String = "Built for"
var words: [String] = ["iOS", "Flutter", "both"]
var font: Font = .system(size: 32, weight: .bold)
var accent: Color = Color(red: 0.15, green: 0.45, blue: 1.0)
/// Seconds each word is on screen.
var interval: Double = 1.8
var phase: Double? = nil
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var animated: Double = 0
private var current: Double { phase ?? animated }
private var lineHeight: CGFloat { 42 }
var body: some View {
HStack(alignment: .center, spacing: 10) {
Text(prefix)
.font(font)
window
}
.onAppear(perform: start)
.accessibilityElement()
.accessibilityLabel("\(prefix) \(words.joined(separator: ", "))")
}
private var window: some View {
// The whole strip is offset by one continuous value; clipping to a
// single line turns that into a rotation.
let total = current * Double(words.count)
let offset = CGFloat(total) * lineHeight
return VStack(spacing: 0) {
// The first word is repeated at the end so the wrap is seamless.
ForEach(Array((words + [words.first ?? ""]).enumerated()), id: \.offset) { _, word in
Text(word)
.font(font)
.foregroundStyle(accent)
.frame(height: lineHeight)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.offset(y: -offset)
.frame(width: widestWord, height: lineHeight, alignment: .topLeading)
.clipped()
}
/// Reserve the width of the longest word so the lead-in never shifts.
private var widestWord: CGFloat {
let longest = words.map(\.count).max() ?? 4
return CGFloat(longest) * 19
}
private func start() {
guard phase == nil, !reduceMotion else { return }
withAnimation(
.linear(duration: interval * Double(words.count)).repeatForever(autoreverses: false)
) {
animated = 1
}
}
}
#Preview {
RotatingText()
.padding(30)
}
iOS 17 · No dependencies
SwiftUI note. The window is a fixed width based on the longest word, so the lead-in never shifts. Widen the multiplier if you use a wider face than the system font.
Dependencies
- SwiftUI
- No external dependencies
- Flutter
- No external dependencies
Requires iOS 17
Requires Flutter 3.16


