
Variable Weight Text
From syxUI — written for both platforms, not translated between them.
A wave of weight rolls through the word, thickening glyphs as it passes.
Text Effects · SwiftUI · Flutter · iOS 17 · Flutter 3.27
- text
- variable font
- weight
- wave
- typography
- kinetic
- animation
The actual source
VariableWeightText.swift
// Variable Weight Text · syxUI · https://syxui.dev/components/variable-weight-text
// Free in any project. Keep this line and credit syxUI where a person can read it.
import SwiftUI
#if os(macOS)
import AppKit
private typealias PlatformFont = NSFont
private typealias PlatformFontDescriptor = NSFontDescriptor
#else
import UIKit
private typealias PlatformFont = UIFont
private typealias PlatformFontDescriptor = UIFontDescriptor
#endif
/// A wave of weight rolls through the word, thickening glyphs as it passes.
///
/// A raised-cosine bump travels left to right through the string; every
/// character's weight is a continuous function of its distance from the
/// bump centre, so neighbours are always mid-transition and the effect
/// reads as gooey rather than as a staircase of discrete weight jumps.
/// `.font()` is not animatable, so the driver is `TimelineView(.animation)`.
struct VariableWeightText: View {
var text: String = "elastic"
var minWeight: Double = 300
var maxWeight: Double = 800
/// Width of the travelling bump, in characters.
var waveWidth: Double = 3.5
var period: Double = 2400
/// Sweeps the `wdth` axis (85...115) alongside weight.
var widthAxis: Bool = false
var fontSize: CGFloat = 40
/// Preview override. When set, the component renders that point of one
/// period instead of driving its own clock.
var phase: Double? = nil
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var characters: [Character] { Array(text) }
var body: some View {
Group {
if reduceMotion && phase == nil {
staticRow
} else {
TimelineView(.animation) { context in
let elapsed = phase ?? (context.date.timeIntervalSinceReferenceDate
.truncatingRemainder(dividingBy: period / 1000) / (period / 1000))
// The bump travels the full string width plus one wave
// width of run-in/run-out, so it starts and ends off-frame.
let span = Double(characters.count) + waveWidth
let position = elapsed * span - waveWidth / 2
row(bumpPosition: position)
}
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(text)
}
/// Reduce Motion: every glyph at the midpoint weight, no travelling
/// bump — a settled frame rather than a wave frozen mid-flight.
private var staticRow: some View {
let midWeight = (minWeight + maxWeight) / 2
return HStack(spacing: 0) {
ForEach(Array(characters.enumerated()), id: \.offset) { _, ch in
Text(String(ch)).font(variableFont(weight: midWeight, width: 100))
}
}
}
private func row(bumpPosition: Double) -> some View {
HStack(spacing: 0) {
ForEach(Array(characters.enumerated()), id: \.offset) { i, ch in
let distance = abs(Double(i) - bumpPosition)
// Raised cosine: 1 at the bump centre, 0 at ±waveWidth/2, and
// smooth (not just clamped-linear) in between.
let normalised = min(distance / (waveWidth / 2), 1)
let intensity = (cos(normalised * .pi) + 1) / 2
let weight = minWeight + (maxWeight - minWeight) * intensity
let width = widthAxis ? 85 + 30 * intensity : 100
let scale = 1 + 0.02 * intensity
Text(String(ch))
.font(variableFont(weight: weight, width: width))
.scaleEffect(scale, anchor: .bottom)
}
}
}
/// Reaches through CoreText for a continuous weight (and optionally
/// width) axis — SwiftUI's `Font.Weight` is a discrete enum and cannot
/// express this. `PlatformFont` is `NSFont` on macOS and `UIFont` on
/// iOS; both expose the same CoreText variation attribute, so this is
/// one code path rather than two.
private func variableFont(weight: Double, width: Double) -> Font {
var variations: [Int: Double] = [2003265652: weight] // 'wght'
if widthAxis { variations[2003072104] = width } // 'wdth'
let axes: [PlatformFontDescriptor.AttributeName: Any] = [
kCTFontVariationAttribute as PlatformFontDescriptor.AttributeName: variations
]
let base = PlatformFont.systemFont(ofSize: fontSize)
let descriptor = base.fontDescriptor.addingAttributes(axes)
#if os(macOS)
// NSFont(descriptor:size:) is failable where UIFont's is not — fall
// back to the unvaried base font rather than force-unwrapping.
let resolved = PlatformFont(descriptor: descriptor, size: fontSize) ?? base
#else
let resolved = PlatformFont(descriptor: descriptor, size: fontSize)
#endif
return Font(resolved as CTFont)
}
}
#Preview {
VariableWeightText()
.frame(width: 320, height: 120)
}
iOS 17 · No dependencies
SwiftUI note. Reaches through CoreText's `kCTFontVariationAttribute` on `PlatformFont` (`NSFont` on macOS, `UIFont` on iOS — one code path, not a feature fork) for a continuous `wght`/`wdth` axis, since `Font.Weight` is a discrete enum. Works with SF Pro directly, no bundled font needed. `NSFont(descriptor:size:)` is failable where `UIFont`'s is not — the macOS branch falls back to the unvaried base font rather than force-unwrapping.
Dependencies
- SwiftUI
- No external dependencies
- Flutter
- No external dependencies
Requires iOS 17
Requires Flutter 3.27


