designengineer.ing100% left
designengineer.ing

The dawn of new aesthetics - Part 2

design engineering

Before you step

Fork swiftuibuttons and create ArrowButton.swift. Paste the constants and colours first — the same tokens you locked in Part 1 before touching strokes or blur.

Each section below adds the next layer. The step titles match ArrowButtonStep.title in the repo and the labels in the video above.

ArrowDateSelector›ArrowButton.swift
1import SwiftUI
2 
3// MARK: - Constants
4private enum ArrowButtonMetrics {
5 static let buttonSize: CGFloat = 125
6 static let reflectionDiameter: CGFloat = 90
7 static let reflectionBlur: CGFloat = 8
8 static let reflectionYOffset: CGFloat = 14
9 static let strokeLineWidth: CGFloat = 3
10 static let arrowShadowOpacity: CGFloat = 0.1
11 static let arrowShadowRadius: CGFloat = 1
12 static let arrowShadowYOffset: CGFloat = 1
13}
14 
15// MARK: - Colors
16private extension Color {
17 static let buttonFill = Color(red: 120/255, green: 133/255, blue: 141/255)
18}
19 
20private extension LinearGradient {
21 static var reflectionFill: LinearGradient {
22 LinearGradient(
23 stops: [
24 Gradient.Stop(color: Color(red: 0.48, green: 0.52, blue: 0.55), location: 0.00),
25 Gradient.Stop(color: Color(red: 0.64, green: 0.67, blue: 0.70), location: 1.00),
26 ],
27 startPoint: .top,
28 endPoint: .bottom
29 )
30 }
31}
SwiftLine 1, Col 1

Base Circle

Add CircularSteelStrokeOverlay, ArrowButtonStep, and ArrowButton. At .base, visibleStrokeCount is 0 — the overlay draws nothing yet. You only see Circle().fill(Color.buttonFill).

showsReflection and showsArrow are still false, so reflectionLayer and the arrow Image stay hidden.

ArrowDateSelector›ArrowButton.swift
1// MARK: - Stroke Overlay
2private struct CircularSteelStrokeOverlay: ViewModifier {
3 /// How many of the four steel strokes are visible (0–4).
4 var visibleStrokeCount: Int
5 
6 func body(content: Content) -> some View {
7 content.overlay(strokeLayer)
8 }
9 
10 private var strokeLayer: some View {
11 ZStack {
12 if visibleStrokeCount >= 1 {
13 Circle()
14 .stroke(
15 LinearGradient(
16 gradient: Gradient(stops: [
17 .init(color: .black.opacity(0.4), location: 0.0),
18 .init(color: .black.opacity(0.0), location: 0.1)
19 ]),
20 startPoint: .leading,
21 endPoint: .topTrailing
22 ),
23 lineWidth: ArrowButtonMetrics.strokeLineWidth
24 )
25 .blendMode(.darken)
26 }
27 
28 if visibleStrokeCount >= 2 {
29 Circle()
30 .stroke(
31 LinearGradient(
32 gradient: Gradient(stops: [
33 .init(color: .black.opacity(0.4), location: 0.0),
34 .init(color: .black.opacity(0.0), location: 0.1)
35 ]),
36 startPoint: .trailing,
37 endPoint: .topLeading
38 ),
39 lineWidth: ArrowButtonMetrics.strokeLineWidth
40 )
41 .blendMode(.darken)
42 }
43 
44 if visibleStrokeCount >= 3 {
45 Circle()
46 .stroke(
47 LinearGradient(
48 gradient: Gradient(stops: [
49 .init(color: .white.opacity(0.8), location: 0.1),
50 .init(color: .white.opacity(0.0), location: 0.4)
51 ]),
52 startPoint: .bottom,
53 endPoint: .top
54 ),
55 lineWidth: ArrowButtonMetrics.strokeLineWidth
56 )
57 .blendMode(.overlay)
58 }
59 
60 if visibleStrokeCount >= 4 {
61 Circle()
62 .stroke(
63 LinearGradient(
64 gradient: Gradient(stops: [
65 .init(color: .white.opacity(0.8), location: 0.0),
66 .init(color: .white.opacity(0.0), location: 0.3)
67 ]),
68 startPoint: .top,
69 endPoint: .bottom
70 ),
71 lineWidth: ArrowButtonMetrics.strokeLineWidth
72 )
73 .blendMode(.normal)
74 }
75 }
76 .compositingGroup()
77 }
78}
79 
80private extension View {
81 func circularSteelStrokeOverlay(visibleStrokeCount: Int = 4) -> some View {
82 modifier(CircularSteelStrokeOverlay(visibleStrokeCount: visibleStrokeCount))
83 }
84}
SwiftLine 1, Col 1
ArrowDateSelector›ArrowButton.swift
1// MARK: - Build Steps (matches design breakdown progression)
2enum ArrowButtonStep: Int, CaseIterable, Comparable {
3 case base = 1
4 case strokeDarkLeading = 2
5 case strokeDarkTrailing = 3
6 case strokeHighlightBottom = 4
7 case strokeHighlightTop = 5
8 case reflection = 6
9 case reflectionBlur = 7
10 case arrow = 8
11 case complete = 9
12 
13 static func < (lhs: ArrowButtonStep, rhs: ArrowButtonStep) -> Bool {
14 lhs.rawValue < rhs.rawValue
15 }
16 
17 var title: String {
18 switch self {
19 case .base: "Base Circle"
20 case .strokeDarkLeading: "Dark Edge I"
21 case .strokeDarkTrailing: "Dark Edge II"
22 case .strokeHighlightBottom: "Light Edge I"
23 case .strokeHighlightTop: "Light Edge II"
24 case .reflection: "Reflection"
25 case .reflectionBlur: "Frosted Blur"
26 case .arrow: "Arrow Icon"
27 case .complete: "Complete"
28 }
29 }
30 
31 var visibleStrokeCount: Int {
32 switch self {
33 case .base: 0
34 case .strokeDarkLeading: 1
35 case .strokeDarkTrailing: 2
36 case .strokeHighlightBottom: 3
37 case .strokeHighlightTop, .reflection, .reflectionBlur, .arrow, .complete: 4
38 }
39 }
40 
41 var reflectionBlurRadius: CGFloat {
42 switch self {
43 case .reflection: 0
44 case .reflectionBlur, .arrow, .complete: ArrowButtonMetrics.reflectionBlur
45 default: 0
46 }
47 }
48 
49 var showsReflection: Bool {
50 rawValue >= ArrowButtonStep.reflection.rawValue
51 }
52 
53 var showsArrow: Bool {
54 self == .arrow || self == .complete
55 }
56}
SwiftLine 1, Col 1
ArrowDateSelector›ArrowButton.swift
1// MARK: - Arrow Button
2struct ArrowButton: View {
3 var rotation: Angle = .zero
4 var step: ArrowButtonStep = .complete
5 
6 var body: some View {
7 ZStack {
8 buttonFace
9 
10 if step.showsArrow {
11 Image("arrow")
12 .rotationEffect(rotation)
13 .shadow(color: .black.opacity(ArrowButtonMetrics.arrowShadowOpacity),
14 radius: ArrowButtonMetrics.arrowShadowRadius,
15 y: ArrowButtonMetrics.arrowShadowYOffset)
16 .accessibilityHidden(true)
17 .transition(.opacity)
18 }
19 }
20 .animation(.easeInOut(duration: 0.3), value: step)
21 .contentShape(Circle())
22 .accessibilityLabel("Arrow button")
23 }
24 
25 private var buttonFace: some View {
26 ZStack {
27 Circle()
28 .fill(Color.buttonFill)
29 
30 reflectionLayer
31 }
32 .frame(width: ArrowButtonMetrics.buttonSize, height: ArrowButtonMetrics.buttonSize)
33 .clipShape(Circle())
34 .circularSteelStrokeOverlay(visibleStrokeCount: step.visibleStrokeCount)
35 }
36 
37 @ViewBuilder
38 private var reflectionLayer: some View {
39 if step.showsReflection {
40 Circle()
41 .fill(LinearGradient.reflectionFill)
42 .frame(
43 width: ArrowButtonMetrics.reflectionDiameter,
44 height: ArrowButtonMetrics.reflectionDiameter
45 )
46 .offset(y: ArrowButtonMetrics.reflectionYOffset)
47 .blur(radius: step.reflectionBlurRadius)
48 .allowsHitTesting(false)
49 .transition(.opacity)
50 }
51 }
52}
SwiftLine 1, Col 1
Figma build progression — core fill layer on the glass button.

Dark Edge I

Step to .strokeDarkLeading. visibleStrokeCount becomes 1 — the first if visibleStrokeCount >= 1 branch in CircularSteelStrokeOverlay draws the leading dark gradient with .blendMode(.darken).

ArrowDateSelector›ArrowButton.swift
1var visibleStrokeCount: Int {
2 switch self {
3 case .base: 0
4 case .strokeDarkLeading: 1
5 case .strokeDarkTrailing: 2
6 case .strokeHighlightBottom: 3
7 case .strokeHighlightTop, .reflection, .reflectionBlur, .arrow, .complete: 4
8 }
9}
SwiftLine 1, Col 1
First metallic stroke on the button edge.

Dark Edge II

At .strokeDarkTrailing, visibleStrokeCount is 2. The second darken stroke runs from trailing toward topLeading — the mirror of Dark Edge I.

ArrowDateSelector›ArrowButton.swift
1var visibleStrokeCount: Int {
2 switch self {
3 case .base: 0
4 case .strokeDarkLeading: 1
5 case .strokeDarkTrailing: 2
6 case .strokeHighlightBottom: 3
7 case .strokeHighlightTop, .reflection, .reflectionBlur, .arrow, .complete: 4
8 }
9}
SwiftLine 1, Col 1
Second dark stroke on the button edge.

Light Edge I

At .strokeHighlightBottom, visibleStrokeCount is 3. The third stroke adds a white gradient along the bottom with .blendMode(.overlay).

ArrowDateSelector›ArrowButton.swift
1var visibleStrokeCount: Int {
2 switch self {
3 case .base: 0
4 case .strokeDarkLeading: 1
5 case .strokeDarkTrailing: 2
6 case .strokeHighlightBottom: 3
7 case .strokeHighlightTop, .reflection, .reflectionBlur, .arrow, .complete: 4
8 }
9}
SwiftLine 1, Col 1
First highlight stroke on the button edge.

Light Edge II

At .strokeHighlightTop, visibleStrokeCount reaches 4 and stays there for every later step. The fourth stroke adds the top highlight with .blendMode(.normal).

ArrowDateSelector›ArrowButton.swift
1var visibleStrokeCount: Int {
2 switch self {
3 case .base: 0
4 case .strokeDarkLeading: 1
5 case .strokeDarkTrailing: 2
6 case .strokeHighlightBottom: 3
7 case .strokeHighlightTop, .reflection, .reflectionBlur, .arrow, .complete: 4
8 }
9}
SwiftLine 1, Col 1
Fourth stroke completing the metallic ring.

Reflection

At .reflection, showsReflection turns on and reflectionBlurRadius is still 0. reflectionLayer draws the vertical grey gradient circle, offset down by reflectionYOffset.

ArrowDateSelector›ArrowButton.swift
1// MARK: - Arrow Button
2struct ArrowButton: View {
3 var rotation: Angle = .zero
4 var step: ArrowButtonStep = .complete
5 
6 var body: some View {
7 ZStack {
8 buttonFace
9 
10 if step.showsArrow {
11 Image("arrow")
12 .rotationEffect(rotation)
13 .shadow(color: .black.opacity(ArrowButtonMetrics.arrowShadowOpacity),
14 radius: ArrowButtonMetrics.arrowShadowRadius,
15 y: ArrowButtonMetrics.arrowShadowYOffset)
16 .accessibilityHidden(true)
17 .transition(.opacity)
18 }
19 }
20 .animation(.easeInOut(duration: 0.3), value: step)
21 .contentShape(Circle())
22 .accessibilityLabel("Arrow button")
23 }
24 
25 private var buttonFace: some View {
26 ZStack {
27 Circle()
28 .fill(Color.buttonFill)
29 
30 reflectionLayer
31 }
32 .frame(width: ArrowButtonMetrics.buttonSize, height: ArrowButtonMetrics.buttonSize)
33 .clipShape(Circle())
34 .circularSteelStrokeOverlay(visibleStrokeCount: step.visibleStrokeCount)
35 }
36 
37 @ViewBuilder
38 private var reflectionLayer: some View {
39 if step.showsReflection {
40 Circle()
41 .fill(LinearGradient.reflectionFill)
42 .frame(
43 width: ArrowButtonMetrics.reflectionDiameter,
44 height: ArrowButtonMetrics.reflectionDiameter
45 )
46 .offset(y: ArrowButtonMetrics.reflectionYOffset)
47 .blur(radius: step.reflectionBlurRadius)
48 .allowsHitTesting(false)
49 .transition(.opacity)
50 }
51 }
52}
SwiftLine 1, Col 1
Reflection layer behind the button face.

Frosted Blur

At .reflectionBlur, reflectionBlurRadius switches to ArrowButtonMetrics.reflectionBlur. The same reflectionLayer softens into frost — the same beat as Part 1’s blur step.

ArrowDateSelector›ArrowButton.swift
1var reflectionBlurRadius: CGFloat {
2 switch self {
3 case .reflection: 0
4 case .reflectionBlur, .arrow, .complete: ArrowButtonMetrics.reflectionBlur
5 default: 0
6 }
7}
8 
9var showsReflection: Bool {
10 rawValue >= ArrowButtonStep.reflection.rawValue
11}
12 
13var showsArrow: Bool {
14 self == .arrow || self == .complete
15}
SwiftLine 1, Col 1
Frosted blur on the reflection layer.

Arrow Icon

At .arrow, showsArrow is true. The body adds Image("arrow") with rotationEffect and a light drop shadow. Pass rotation for previous vs next in the date selector.

ArrowDateSelector›ArrowButton.swift
1// MARK: - Arrow Button
2struct ArrowButton: View {
3 var rotation: Angle = .zero
4 var step: ArrowButtonStep = .complete
5 
6 var body: some View {
7 ZStack {
8 buttonFace
9 
10 if step.showsArrow {
11 Image("arrow")
12 .rotationEffect(rotation)
13 .shadow(color: .black.opacity(ArrowButtonMetrics.arrowShadowOpacity),
14 radius: ArrowButtonMetrics.arrowShadowRadius,
15 y: ArrowButtonMetrics.arrowShadowYOffset)
16 .accessibilityHidden(true)
17 .transition(.opacity)
18 }
19 }
20 .animation(.easeInOut(duration: 0.3), value: step)
21 .contentShape(Circle())
22 .accessibilityLabel("Arrow button")
23 }
24 
25 private var buttonFace: some View {
26 ZStack {
27 Circle()
28 .fill(Color.buttonFill)
29 
30 reflectionLayer
31 }
32 .frame(width: ArrowButtonMetrics.buttonSize, height: ArrowButtonMetrics.buttonSize)
33 .clipShape(Circle())
34 .circularSteelStrokeOverlay(visibleStrokeCount: step.visibleStrokeCount)
35 }
36 
37 @ViewBuilder
38 private var reflectionLayer: some View {
39 if step.showsReflection {
40 Circle()
41 .fill(LinearGradient.reflectionFill)
42 .frame(
43 width: ArrowButtonMetrics.reflectionDiameter,
44 height: ArrowButtonMetrics.reflectionDiameter
45 )
46 .offset(y: ArrowButtonMetrics.reflectionYOffset)
47 .blur(radius: step.reflectionBlurRadius)
48 .allowsHitTesting(false)
49 .transition(.opacity)
50 }
51 }
52}
SwiftLine 1, Col 1
Arrow vector on the glass button.

Complete

At .complete, every layer is on — four strokes, frosted reflection, and the arrow. ArrowButtonTutorial drives the same step enum with chevron prev/next, matching the video above.

When the tutorial reaches .complete, the button becomes tappable and opens ArrowDateSelector via onOpenDateSelector in ContentView.swift.

ArrowDateSelector›ArrowButton.swift
1// MARK: - Tutorial
2private struct ArrowButtonPressStyle: ButtonStyle {
3 func makeBody(configuration: Configuration) -> some View {
4 configuration.label
5 .scaleEffect(configuration.isPressed ? 0.96 : 1)
6 .animation(.easeInOut(duration: 0.15), value: configuration.isPressed)
7 }
8}
9 
10struct ArrowButtonTutorial: View {
11 var onOpenDateSelector: (() -> Void)? = nil
12 @State private var currentStep: ArrowButtonStep = .base
13 
14 var body: some View {
15 ZStack {
16 Color(red: 93/255, green: 106/255, blue: 114/255)
17 .ignoresSafeArea()
18 
19 VStack(spacing: 0) {
20 Group {
21 if currentStep != .complete {
22 Text(currentStep.title)
23 .font(.system(size: 17, weight: .medium))
24 .foregroundStyle(.white)
25 .contentTransition(.numericText())
26 .animation(.easeInOut(duration: 0.2), value: currentStep)
27 } else {
28 Color.clear
29 }
30 }
31 .frame(height: 28)
32 .padding(.top, 72)
33 
34 Spacer()
35 
36 Group {
37 if currentStep == .complete {
38 Button {
39 HapticsHelper.impactMedium()
40 onOpenDateSelector?()
41 } label: {
42 ArrowButton(step: .complete)
43 }
44 .buttonStyle(ArrowButtonPressStyle())
45 .accessibilityLabel("Arrow button")
46 } else {
47 ArrowButton(step: currentStep)
48 .allowsHitTesting(false)
49 }
50 }
51 
52 Spacer()
53 
54 HStack(spacing: 48) {
55 Button {
56 if let previous = ArrowButtonStep(rawValue: currentStep.rawValue - 1) {
57 currentStep = previous
58 }
59 } label: {
60 Image(systemName: "chevron.left")
61 .font(.system(size: 18, weight: .semibold))
62 .frame(width: 44, height: 44)
63 .background(Circle().fill(.white.opacity(currentStep == .base ? 0.08 : 0.2)))
64 .foregroundStyle(.white.opacity(currentStep == .base ? 0.3 : 1))
65 }
66 .buttonStyle(.plain)
67 .disabled(currentStep == .base)
68 
69 Button {
70 if let next = ArrowButtonStep(rawValue: currentStep.rawValue + 1) {
71 currentStep = next
72 }
73 } label: {
74 Image(systemName: "chevron.right")
75 .font(.system(size: 18, weight: .semibold))
76 .frame(width: 44, height: 44)
77 .background(Circle().fill(.white.opacity(currentStep == .complete ? 0.08 : 0.2)))
78 .foregroundStyle(.white.opacity(currentStep == .complete ? 0.3 : 1))
79 }
80 .buttonStyle(.plain)
81 .disabled(currentStep == .complete)
82 }
83 .padding(.bottom, 60)
84 }
85 }
86 }
87}
88 
89#Preview("Tutorial") {
90 ArrowButtonTutorial()
91 .accessibilityHidden(true)
92}
SwiftLine 1, Col 1
Completed glass arrow button.

0 from readers

A shout out is a small burst of encouragement. When you tap 📢, you let us know this tutorial helped — and that keeps us happy and motivated to write more like it.

Explore

  • Glass arrow — Part 1
  • Glass arrow — Part 2
  • Rive balloons
  • YouTube Playground
  • Pinterest Playground

we are also on Follow

design engineer.ing

© 2026 design engineering

·Design engineering