TailwindSwiftUI Tailwind Templates

Tailwindswiftui

A utility-first SwiftUI framework for rapid UI development.

Rapidly build modern apps without ever leaving your SwiftUI.

A utility-first Swift package packed with view modifiers that can be composed to build any design, directly in your SwiftUI code.

Get started

Core Concepts


Core Concepts

Utility-First Fundamentals

Building complex components from a constrained set of primitive utilities.


Traditionally, whenever you need to style something on the SwiftUI, you write view modifiers.

❌ Using a traditional approach where custom designs require custom values.

Screenshot 2024-03-31 at 4 37 29 PM
HStack(spacing: 13) {
    Image("logo")
        .resizable()
        .frame(width: 39, height: 39)
    VStack(alignment: .leading) {
        Text("ChitChat")
            .font(.system(size: 16.25))
            .lineSpacing(6.5)
            .fontWeight(.medium)
            .foregroundStyle(.black)
        Text("You have a new message!")
            .foregroundStyle(Color(red: 100 / 255, green: 116 / 255, blue: 139 / 255, opacity: 1))
    }
    .frame(maxWidth: .infinity, alignment: .leading)
}
.padding(20)
.frame(maxWidth: 312)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
.background {
    Color.black.opacity(0.1)
        .padding(.horizontal, 3)
        .padding(.vertical, 3)
        .offset(x: 0, y: 10)
        .blur(radius: 15)
}
.background {
    Color.black.opacity(0.1)
        .padding(.horizontal, 4)
        .padding(.vertical, 4)
        .offset(x: 0, y: 4)
        .blur(radius: 6)
}

With TailwindSwiftUI, you style elements by applying pre-existing view modifiers directly in your SwiftUI.

✅ Using utility view modifiers to build custom designs without writing values

Screenshot 2024-03-31 at 4 05 07 PM
HStack(spacing: .s4) {
    Image("logo")
        .resizable()
        .width(.s12)
        .height(.s12)
    VStack(alignment: .leading) {
        Text("ChitChat")
            .text(.extraLarge)
            .fontWeight(.medium)
            .foregroundStyle(.black)
        Text("You have a new message!")
            .foregroundStyle(.slate500)
    }
    .width(.full, alignment: .leading)
}
.padding(.s6)
.max(width: .small)
.background(.white)
.rounded(.extraLarge)
.shadow(.large)

In the example above, we've used:

  • Tailwind SwiftUI's padding utilities (.padding(.s6)) to control the overall card layout

  • The max-width utilities (.max(width:.small)) to constrain the card width.

  • The border radius, and box-shadow utilities (.rounded(.extraLarge), .shadow(.large)) to style the card's appearance

  • The width and height utilities (.width(.s12), .height(.s12)) to size the logo image

  • The spacing utilities (spacing: .s4) to handle the spacing between the logo and the text

  • The font size utilities (.text(.extraLarge)) to style the card text

This approach allows us to implement a completely custom component design without writing a single custom value.

Now I know what you're thinking, "this is an atrocity, what a horrible mess!" and you're right, it's kind of ugly. In fact it's just about impossible to think this is a good idea the first time you see it -- you have to actually try it.

But once you've actually built something this way, you'll quickly notice some really important benefits:

  • Designing with constraints. Using view modifiers, every value is a magic number. With utilities, you're choosing styles from a predefined design system, which makes it much easier to build visually consistent UIs.

  • Responsive design. You can't use media queries in view modifiers, but you can use Tailwind SwiftUI's responsive utilities to build fully responsive interface easily.

  • Hover, focus, and other states. View modifiers can't target states like hover or focus, but Tailwind SwiftUI's state variants make it easy to style these states with utility view modifiers.

This component is fully responsive and includes a button with hover styles, and is built entirely with utility views and view modifiers:

Flex(.small, horizontalSpacing: .s4, verticalSpacing: .s2) {
    AsyncImage(url: .init(string: "https://tailwindcss.com/img/erin-lindford.jpg")) { image in
        image
            .resizable()
            .scaledToFit()
            .height(.s24)
            .rounded(.full)
    } placeholder: {
        ProgressView()
    }
    .accessibilityLabel("Woman's Face")
    VStack(alignment: .leading, spacing: .s3_5) {
        VStack(alignment: .leading, spacing: .s1_5) {
            Text("Erin Lindford")
                .text(.large)
                .foregroundStyle(.black)
                .fontWeight(.semibold)
                .small(otherwise: .width(.full, alignment: .center))
            Text("Product Engineer")
                .foregroundStyle(.slate500)
                .fontWeight(.medium)
                .small(otherwise: .width(.full, alignment: .center))
        }
        Button {} label: {
            Text("Message")
                .padding(.horizontal, .s4)
                .padding(.vertical, .s1)
                .text(.small)
                .hover { content in
                    content
                        .foregroundStyle(.white)
                        .background(.purple600)
                        .ring(.purple600, thickness: .t2, offset: .init(width: .s2))
                        .rounded(.full)
                } otherwise: { content in
                    content
                        .foregroundStyle(.purple600)
                        .border(.purple200, rounded: .full)
                }
                .fontWeight(.semibold)
                .cursorPointer()
        }
        .small(otherwise: .width(.full, alignment: .center))
        .buttonStyle(.plain)
    }
}
.padding(.s8)
.max(width: .small)
.background(.white)
.rounded(.extraLarge)
.shadow(.large)
.main()

When you realize how productive you can be working exclusively in SwiftUI with predefined view modifiers, working any other way will feel like torture.

Core Concepts

Handling Hover, Focus, and Other States

Using utilities to style elements on hover, focus, and more.

Every utility view modifier in Tailwind SwiftUI can be applied conditionally by adding a modifier to the beginning of the view modifier that describes the condition you want to target.

For example, to apply the .background(.sky700) view modifier on hover, use the .hover(.background(.sky700)) view modifier:

Screenshot 2024-04-01 at 2 26 54 PM

Button {} label: {
    Text("Save changes")
        ...
        .hover(.background(.sky700), default: .background(.sky500))
        ...
}

How does this compare to traditional SwiftUI?

When writing SwiftUI the traditional way, a @State property would be needed to track the hover state and update the view modifier based on the current state.

❌ Traditionally the hover state would be tracked with a @State property and a onHover modifier.

@State private var isHovering = false

var body: some View {
    Button {} label: {
        Text("Save changes")
            .background(isHovering ? Color(red: 3 / 255, green: 105 / 255, blue: 161 / 255) : Color(red: 14 / 255, green: 165 / 255, blue: 233 / 255))
    }
    .onHover { hovering in
        isHovering = hovering
    }
}

In Tailwind SwiftUI, rather than adding the styles for a hover state to an exsiting view, you can add another view modifier to the element that only does something on hover.

✅ In Tailwind SwiftUI, the .hover(_, default:) view modifier is used for the hover state and the default state

.hover(.background(.sky700), default: .background(.sky500))

Notice how .hover(.background(.sky700), default: .background(.sky500)) defines styles for the hover state and the default state? The background is .sky500 by default, but as soon as you hover an element with that view modifier, the background will change to .sky700.

This is what we mean when we say a utility view modifier can be applied conditionally — by using modifiers you can control exactly how your design behaves in different states, without ever tracking states yourself.

Tailwind SwiftUI includes modifiers for just about everything you'll ever need, including:

  • View modifiers, like .hover(_) and .focus(_).
  • Transforms, like .hover(_ hoverTransform:).
  • Media and feature queries, like responsive breakpoints and dark mode.

These modifiers can even be stacked to target more specific situations, for example changing the background color in dark mode, at the medium breakpoint, on hover:

Button {} label: {
    Text("Save changes")
}
.dark(.medium(.hover(.background(.fuchsia600))))
...

Screenshot 2024-03-16 at 3 12 35 PM Screenshot 2024-03-16 at 3 13 45 PM
Flex(.medium) {
    AsyncImage(
        url: .init(string: "https://images.unsplash.com/photo-1706820643404-71812d9d7d3a?w=384"),
        content: { image in
            image
                .resizable()
                .scaledToFill()
        }, placeholder: {
            ProgressView()
        }
    )
    .width(.s24)
    .height(.s24)
    .medium(.width(.s48))
    .medium(.height(.auto))
    .medium(.rounded(.none), otherwise: .rounded(.full))
    VStack(spacing: .scale(.s4)) {
        Group {
            Text("4 Hikers perched a top Wanaka's highest mountain to watch the sun rise in New Zealand's South Island")
                .text(.large)
            VStack {
                Group {
                    Text("Nick Da Fonseca")
                        .foregroundStyle(.sky500)
                        .dark(.foregroundStyle(.sky400))
                    Text("Roys Peak, Wānaka, New Zealand")
                        .dark(.foregroundStyle(.slate500), otherwise: .foregroundStyle(.slate700))
                }
                .medium(.frame(maxWidth: .infinity, alignment: .leading))
            }
        }
        .medium(.frame(maxWidth: .infinity, alignment: .leading))
        .medium(.multilineTextAlignment(.leading), otherwise: .multilineTextAlignment(.center))
        .fontWeight(.medium)
    }
    .medium(.padding(.s8), otherwise: .padding(.top, .s6))
}
.medium(.padding(.s0), otherwise: .padding(.s8))
.background(.slate100)
.dark(.background(.slate800))
.rounded(.extraLarge)

Constraint-based

An API for your design system.

Utility view modifiers help your work within the constraints of a system instead of littering your SwiftUI with arbitrary values. They make it easy to be consistent with color choices, spacing, typography, shadows, and everything else that makes up a well-engineered design system.

Sizing

Screenshot 2024-03-16 at 4 19 39 PM
VStack(alignment: .leading, spacing: .scale(.s4)) {
    Group {
        Text(".width(.s96)")
            .width(.s96)
        Text(".width(.s80)")
            .width(.s80)
        Text(".width(.s72)")
            .width(.s72)
        Text(".width(.s64)")
            .width(.s64)
        Text(".width(.s60)")
            .width(.s60)
        Text(".width(.s56)")
            .width(.s56)
        Text(".width(.s52)")
            .width(.s52)
        Text(".width(.s48)")
            .width(.s48)
        Text(".width(.s44)")
            .width(.s44)
        Text(".width(.s40)")
            .width(.s40)
        Text(".width(.s36)")
            .width(.s36)
    }
    .background(.white)
    .rounded()
    .shadow()
}

Colors

Screenshot 2024-03-16 at 5 08 20 PM
LazyVGrid(columns: Array(repeating: .init(.flexible(), spacing: .scale(.s2)), count: 10)) {
    Group {
        Color.sky50
        Color.sky100
        Color.sky200
        Color.sky300
        Color.sky400
        Color.sky500
        Color.sky600
        Color.sky700
        Color.sky800
        Color.sky900
    }
    .aspectRatio(1, contentMode: .fit)
}
LazyVGrid(columns: Array(repeating: .init(.flexible(), spacing: .scale(.s2)), count: 10)) {
    Group {
        Color.blue50
        Color.blue100
        Color.blue200
        Color.blue300
        Color.blue400
        Color.blue500
        Color.blue600
        Color.blue700
        Color.blue800
        Color.blue900
    }
    .aspectRatio(1, contentMode: .fit)
}
LazyVGrid(columns: Array(repeating: .init(.flexible(), spacing: .scale(.s2)), count: 10)) {
    Group {
        Color.indigo50
        Color.indigo100
        Color.indigo200
        Color.indigo300
        Color.indigo400
        Color.indigo500
        Color.indigo600
        Color.indigo700
        Color.indigo800
        Color.indigo900
    }
    .aspectRatio(1, contentMode: .fit)
}
LazyVGrid(columns: Array(repeating: .init(.flexible(), spacing: .scale(.s2)), count: 10)) {
    Group {
        Color.violet50
        Color.violet100
        Color.violet200
        Color.violet300
        Color.violet400
        Color.violet500
        Color.violet600
        Color.violet700
        Color.violet800
        Color.violet900
    }
    .aspectRatio(1, contentMode: .fit)
}

Typography

Screenshot 2024-03-16 at 5 41 05 PM
VStack(alignment: .leading, spacing: .scale(.s5)) {
    Group {
        VStack(alignment: .leading) {
            Text(".fontDesign(.default)")
                .text(.small)
                .border(.bottom)
            Text("The quick brown fox jumps over the lazy dog.")
                .fontDesign(.default)
        }
        VStack(alignment: .leading) {
            Text(".fontDesign(.serif)")
                .text(.small)
                .border(.bottom)
            Text("The quick brown fox jumps over the lazy dog.")
                .fontDesign(.serif)
        }
        VStack(alignment: .leading) {
            Text(".fontDesign(.monospaced)")
                .text(.small)
                .border(.bottom)
            Text("The quick brown fox jumps over the lazy dog.")
                .fontDesign(.monospaced)
        }
    }
    .padding(.s3)
    .background(.white)
    .rounded(.large)
    .shadow()
}

Shadows

Screenshot 2024-03-17 at 10 43 06 AM
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: .scale(.s4)), count: 2), spacing: .scale(.s6)) {
    Group {
        Text(".shadow(.small)")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
            .background(.white)
            .rounded(.large)
            .boxShadow(.small)
        Text(".shadow()")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
            .background(.white)
            .rounded(.large)
            .boxShadow()
        Text(".shadow(.medium)")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
            .background(.white)
            .rounded(.large)
            .boxShadow(.medium)
        Text(".shadow(.large)")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
            .background(.white)
            .rounded(.large)
            .boxShadow(.large)
        Text(".shadow(.extraLarge)")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
            .background(.white)
            .rounded(.large)
            .boxShadow(.extraLarge)
        Text(".shadow(.extraLarge2)")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
            .background(.white)
            .rounded(.large)
            .boxShadow(.extraLarge2)
    }
    .height(.s20)
}

Build anything

Build whatever you want, seriously.

Simple

Screenshot 2024-03-17 at 4 55 56 PM
HStack {
    AsyncImage(url: .init(string: "https://images.unsplash.com/photo-1708443683202-a5be0ced5b8b")) { image in
        image
            .resizable()
            .scaledToFill()
    } placeholder: {
        ProgressView()
    }
    .width(.s48)
    .height(.full)
    .clipped()
    Form {
        HStack {
            Text("Pullover Hoodie")
                .text(.large)
                .fontWeight(.semibold)
                .foregroundStyle(.slate900)
                .width(.full, alignment: .leading)
            Text("$110.00")
                .text(.large)
                .fontWeight(.semibold)
                .foregroundStyle(.slate500)
        }
        Text("In stock")
            .width(.full, alignment: .leading)
            .text(.small)
            .fontWeight(.medium)
            .foregroundStyle(.slate700)
            .padding(.top, .s2)
        HStack(alignment: .firstTextBaseline, spacing: .scale(.s2)) {
            ForEach(["XS", "S", "M", "L", "XL"], id: \.self) { size in
                Button {
                    selectedSize = size
                } label: {
                    Text(size)
                        .width(.s9)
                        .height(.s9)
                        .if(selectedSize == size) { view in
                            view
                                .fontWeight(.medium)
                                .background(.slate900)
                                .foregroundStyle(.white)
                        } or: { view in
                            view
                                .foregroundStyle(.slate700)
                        }
                        .rounded(.large)
                }
                .accessibilityLabel("\(size) size")
                .accessibilityValue(selectedSize == size ? "selected" : "not selected")
                .buttonStyle(.plain)
            }
        }
        .text(.small)
        .padding(.bottom, .s6)
        .border(.slate200, .bottom)
        .padding(.top, .s4)
        .padding(.bottom, .s6)
        HStack(spacing: .scale(.s4)) {
            HStack(spacing: .scale(.s4)) {
                Button("Buy now") {}
                    .height(.s10)
                    .padding(.horizontal, .s6)
                    .background(.black)
                    .foregroundStyle(.white)
                    .rounded(.medium)
                    .buttonStyle(.plain)
                Button("Add to bag") {}
                    .height(.s10)
                    .padding(.horizontal, .s6)
                    .border(.slate200, rounded: .medium)
                    .foregroundStyle(.slate900)
                    .buttonStyle(.plain)
            }
            .fontWeight(.semibold)
            .width(.full, alignment: .leading)
            Button {} label: {
                Image(systemName: "heart.fill")
            }
            .width(.s9)
            .height(.s9)
            .foregroundStyle(.slate300)
            .border(.slate200, rounded: .medium)
            .accessibilityLabel("Like")
            .buttonStyle(.plain)
        }
        .padding(.bottom, .s6)
        .text(.small)
        .fontWeight(.medium)
        Text("Free shipping on all continental US orders.")
            .text(.small)
            .foregroundStyle(.slate700)
    }
    .padding(.s6)
    .width(.full)
}

Playful

Screenshot 2024-03-17 at 4 59 33 PM
HStack {
    AsyncImage(url: .init(string: "https://images.unsplash.com/photo-1568385247005-0d371d214a2c")) { image in
        image
            .resizable()
            .scaledToFill()
    } placeholder: {
        ProgressView()
    }
    .width(.s56)
    .height(.full)
    .rounded(.large)
    Form {
        HStack {
            Text("Kids Dress")
                .width(.full, alignment: .leading)
                .fontWeight(.medium)
                .foregroundStyle(.slate900)
            Text("In stock")
                .text(.small)
                .fontWeight(.medium)
                .foregroundStyle(.slate400)
        }
        Text("$39.00")
            .padding(.top, .s2)
            .text(.extraLarge3)
            .fontWeight(.bold)
            .foregroundStyle(.violet600)
        HStack(alignment: .firstTextBaseline, spacing: .scale(.s2)) {
            ForEach(["XS", "S", "M", "L", "XL"], id: \.self) { size in
                Button {
                    selectedSize = size
                } label: {
                    Text(size)
                        .width(.s9)
                        .height(.s9)
                        .if(selectedSize == size) { view in
                            view
                                .fontWeight(.medium)
                                .background(.violet600)
                                .foregroundStyle(.white)
                        } or: { view in
                            view
                                .foregroundStyle(.violet400)
                        }
                        .rounded(.full)
                }
                .accessibilityLabel("\(size) size")
                .accessibilityValue(selectedSize == size ? "selected" : "not selected")
                .buttonStyle(.plain)
            }
        }
        .text(.small)
        .fontWeight(.bold)
        .padding(.bottom, .s6)
        .border(.slate200, .bottom)
        .padding(.top, .s4)
        .padding(.bottom, .s6)
        HStack(spacing: .scale(.s4)) {
            HStack(spacing: .scale(.s4)) {
                Button("Buy now") {}
                    .height(.s10)
                    .padding(.horizontal, .s6)
                    .background(.violet600)
                    .foregroundStyle(.white)
                    .rounded(.full)
                    .buttonStyle(.plain)
                Button("Add to bag") {}
                    .height(.s10)
                    .padding(.horizontal, .s6)
                    .border(.slate200, rounded: .full)
                    .foregroundStyle(.slate900)
                    .buttonStyle(.plain)
            }
            .fontWeight(.semibold)
            .width(.full, alignment: .leading)
            Button {} label: {
                Image(systemName: "heart.fill")
            }
            .width(.s9)
            .height(.s9)
            .foregroundStyle(.violet600)
            .background(.violet50)
            .rounded(.full)
            .accessibilityLabel("Like")
            .buttonStyle(.plain)
        }
        .padding(.bottom, .s5)
        .text(.small)
        .fontWeight(.medium)
        Text("Free shipping on all continental US orders.")
            .text(.small)
            .foregroundStyle(.slate500)
    }
    .padding(.s6)
    .width(.full)
}

Elegant

Screenshot 2024-03-18 at 7 53 25 PM
HStack {
    AsyncImage(url: .init(string: "https://images.unsplash.com/photo-1559034750-cdab70a66b8e")) { image in
        image
            .resizable()
            .scaledToFill()
    } placeholder: {
        ProgressView()
    }
    .width(.s52)
    .height(.full)
    .clipped()
    Form {
        Text("Formal Strapless Gown")
            .text(.extraLarge2, leading: nil)
            .padding(.bottom, .s3)
            .foregroundStyle(.slate900)
        HStack(alignment: .firstTextBaseline) {
            Text("$350.00")
                .width(.full, alignment: .leading)
                .text(.large)
                .fontWeight(.medium)
                .foregroundStyle(.slate500)
            Text("In stock")
                .text(.extraSmall, leading: .s6)
                .fontWeight(.medium)
                .textCase(.uppercase)
                .foregroundStyle(.slate500)
        }
        HStack(alignment: .firstTextBaseline, spacing: .scale(.s1)) {
            ForEach(["XS", "S", "M", "L", "XL"], id: \.self) { size in
                Button {
                    selectedSize = size
                } label: {
                    Text(size)
                        .width(.s7)
                        .height(.s7)
                        .if(selectedSize == size) { view in
                            view
                                .fontWeight(.medium)
                                .background(.slate100)
                                .foregroundStyle(.slate900)
                        } or: { view in
                            view
                                .foregroundStyle(.slate500)
                        }
                        .rounded(.full)
                }
                .accessibilityLabel("\(size) size")
                .accessibilityValue(selectedSize == size ? "selected" : "not selected")
                .buttonStyle(.plain)
            }
        }
        .text(.small)
        .fontWeight(.medium)
        .padding(.bottom, .s6)
        .border(.slate200, .bottom)
        .padding(.top, .s4)
        .padding(.bottom, .s6)
        HStack(spacing: .scale(.s4)) {
            HStack(spacing: .scale(.s4)) {
                Button("Buy now") {}
                    .width(.full)
                    .height(.s12)
                    .background(.slate900)
                    .foregroundStyle(.white)
                    .buttonStyle(.plain)
                Button("Add to bag") {}
                    .width(.full)
                    .height(.s12)
                    .border(.slate200)
                    .foregroundStyle(.slate900)
                    .buttonStyle(.plain)
            }
            .textCase(.uppercase)
            .fontWeight(.medium)
            .kerning(.wider)
            .width(.full)
            .padding(.trailing, .s4)
            Button {} label: {
                Image(systemName: "heart.fill")
            }
            .width(.s12)
            .height(.s12)
            .foregroundStyle(.slate300)
            .border(.slate200)
            .accessibilityLabel("Like")
            .buttonStyle(.plain)
        }
        .padding(.bottom, .s5)
        .text(.small)
        .fontWeight(.medium)
        Text("Free shipping on all continental US orders.")
            .text(.small)
            .foregroundStyle(.slate500)
    }
    .padding(.s6)
    .width(.full)
}
.fontDesign(.serif)

Brutalist

Screenshot 2024-03-18 at 11 03 16 PM
HStack {
    AsyncImage(url: .init(string: "https://images.unsplash.com/photo-1593169158019-e33d5a325c4c")) { image in
        image
            .resizable()
            .scaledToFill()
            .frame(width: 148, height: 200)
            .clipped()
            .background {
                Color.teal400
                    .top(.s1)
                    .left(.s1)
                    .bottom(.n1)
                    .right(.n1)
            }
    } placeholder: {
        ProgressView()
    }
    .width(.s48)
    .padding(.bottom, .s10)
    .zIndex(10)
    Form {
        VStack {
            Text("Retro Shoe")
                .width(.full, alignment: .leading)
                .text(.extraLarge2)
                .padding(.bottom, .s2)
                .fontWeight(.semibold)
                .foregroundStyle(.white)
            HStack(alignment: .firstTextBaseline) {
                Text("$350.00")
                    .text(.large)
                    .foregroundStyle(.white)
                Text("In stock")
                    .textCase(.uppercase)
                    .foregroundStyle(.teal400)
                    .padding(.leading, .s3)
            }
            .width(.full, alignment: .leading)
        }
        .padding(.bottom, .s6)
        .background(
            Color.black
                .top(.n6)
                .left(.n64)
                .right(.n6)
        )
        HStack(alignment: .firstTextBaseline, spacing: .scale(.s3)) {
            ForEach(["XS", "S", "M", "L", "XL"], id: \.self) { size in
                Button {
                    selectedSize = size
                } label: {
                    Text(size)
                        .width(.s10)
                        .height(.s10)
                        .if(selectedSize == size) { view in
                            view
                                .background(.black)
                                .foregroundStyle(.white)
                                .background {
                                    Color.teal400
                                        .top(.s0_5)
                                        .left(.s0_5)
                                        .bottom(.n0_5)
                                        .right(.n0_5)
                                }
                        } or: { view in
                            view
                                .foregroundStyle(.black)
                        }
                }
                .accessibilityLabel("\(size) size")
                .accessibilityValue(selectedSize == size ? "selected" : "not selected")
                .buttonStyle(.plain)
            }
        }
        .text(.small)
        .fontWeight(.medium)
        .padding(.vertical, .s6)
        HStack(spacing: .scale(.s2)) {
            HStack(spacing: .scale(.s4)) {
                Button("Buy now") {}
                    .padding(.horizontal, .s6)
                    .height(.s12)
                    .border(.black, width: 2)
                    .background(.teal400)
                    .foregroundStyle(.black)
                Button("Add to bag") {}
                    .padding(.horizontal, .s6)
                    .height(.s12)
                    .border(.slate200)
                    .foregroundStyle(.slate900)
            }
            .textCase(.uppercase)
            .fontWeight(.semibold)
            .kerning(.wider)
            Button {} label: {
                Image(systemName: "heart.fill")
            }
            .width(.s12)
            .height(.s12)
            .foregroundStyle(.black)
            .accessibilityLabel("Like")
        }
        .padding(.bottom, .s4)
        .text(.small)
        .fontWeight(.medium)
        .buttonStyle(.plain)
        Text("Free shipping on all continental US orders.")
            .text(.extraSmall, leading: .s6)
            .foregroundStyle(.slate500)
    }
    .padding(.leading, .s6)
    .width(.full)
}
.fontDesign(.monospaced)

Mobile-first

Responsive everything.

Tailwind SwiftUI lets you build responsive designs right in your SwiftUI.

Throw a screen size in front of literally any view modifier and watch it magically apply at a specific breakpoint.

Screenshot 2024-03-25 at 4 00 56 PM

Small

Screenshot 2024-03-25 at 4 01 30 PM

Medium

Screenshot 2024-03-25 at 4 01 56 PM

Large

Screenshot 2024-03-25 at 4 02 09 PM
var body: some View {
    Main {
        ZStack {
            HStack {
                GridView {
                    Large{} otherwise: {
                        ZStack {
                            images
                            Small {} otherwise: {
                                property
                            }
                        }
                        .rounded(.large)
                    }
                    HStack {
                        VStack {
                            Small {
                                property
                            }
                            HStack {
                                HStack(spacing: 0) {
                                    Image(systemName: "star.fill")
                                        .padding(.trailing, .s1)
                                        .dark(.foregroundStyle(.indigo500))
                                    Text("4.89 ")
                                    Text("(128)")
                                        .foregroundStyle(.slate400)
                                }
                                .accessibilityLabel("Reviews")
                                .foregroundStyle(.indigo600)
                                HStack {
                                    Circle()
                                        .frame(width: 2)
                                        .padding(.horizontal, .s3)
                                        .foregroundStyle(.slate300)
                                    Image(systemName: "mappin.circle")
                                        .padding(.trailing, .s1)
                                        .dark(.foregroundStyle(.slate500), otherwise: .foregroundStyle(.slate400))
                                    Text("Santa Teresa, ES")
                                }
                                .accessibilityLabel("Location")
                            }
                            .small(.padding(.top, .s1), otherwise: .padding(.top, .s4))
                            .text(.small)
                            .fontWeight(.medium)
                            .width(.full, alignment: .leading)
                        }
                        Small {
                            Large {} otherwise: {
                                checkAvailability
                            }
                        }
                    }
                    Small {
                        Large {
                            checkAvailability
                        }
                    } otherwise: {
                        checkAvailability
                    }
                    Text("Discover tranquility in our Cozy Cabin Retreat, nestled in the heart of Santa Teresa, ES, Brazil. This A-frame sanctuary offers stunning mountain views and a unique blend of rustic charm and modern comfort. Perfect for relaxation seekers, our cabin is surrounded by nature yet close to local cafes and cultural spots. Ideal for a serene getaway or a nature-filled adventure.")
                        .large(.padding(.top, .s6), otherwise: .padding(.top, .s4))
                        .text(.small, leading: 6)
                        .width(.full, alignment: .leading)
                        .dark(.foregroundStyle(.slate400))
                }
                .large(.padding(.trailing, .s20))
                Large {
                    images
                }
            }
            .large(.max(width: .extraLarge5), otherwise: .max(width: .extraLarge4))
        }
        .medium(.padding(.vertical, .s10), otherwise: .padding(.vertical, .s6))
        .medium(.padding(.horizontal, .s8), otherwise: .small(.padding(.horizontal, .s6), otherwise: .padding(.horizontal, .s4)))
        .width(.full, alignment: .center)
    }
    .dark(.foregroundStyle(.slate400), otherwise: .foregroundStyle(.slate500))
    .background(.white)
}

var property: some View {
    VStack(alignment: .leading) {
        Text("Entire house")
            .text(.small, leading: 4)
            .fontWeight(.medium)
        Text("Cabin Retreat in Santa Teresa")
            .padding(.top, .s1)
            .text(.large)
            .fontWeight(.semibold)
    }
    .small(.foregroundStyle(.slate900), otherwise: .foregroundStyle(.white))
    .small(otherwise: .padding(.s3))
    .width(.full, alignment: .leading)
    .small(otherwise: .height(.s60, alignment: .bottomLeading))
    .small(.background(.clear), otherwise: .backgroundGradient(to: .right, from: .black / 75, via: .black / 0))
}

var checkAvailability: some View {
    Button {} label: {
        Text("Check availability")
            .foregroundStyle(.white)
            .text(.small, leading: 6)
            .fontWeight(.medium)
            .padding(.vertical, .s2)
            .padding(.horizontal, .s3)
            .background(.indigo600)
            .rounded(.large)
    }
    .small(.large(.padding(.top, .s6)), otherwise: .padding(.top, .s4))
    .small(.large(.width(.full, alignment: .leading), otherwise: .width(.full, alignment: .trailing)), otherwise: .width(.full, alignment: .leading))
    .buttonStyle(.plain)
}

var images: some View {
    GridView(smallCols: 4, .gap(.s4), large: .gap(.s6)) {
        Image("CabinRetreat")
            .resizable()
            .scaledToFill()
            .small(.height(.s52), otherwise: .height(.s60))
            .rounded(.large)
            .small(.col(span: 2))
            .large(.col(span: .full))
        Group {
            Object(.cover) {
                Image("CabinRetreatInterior")
                .resizable()
            }
            .large(.col(span: 2), otherwise: .medium(.col(span: 1), otherwise: .col(span: 2)))
            .small(.height(.s52), otherwise: .height(.s0))
            .large(.height(.s32))
            Object(.cover) {
                Image("CabinRetreatExterior")
                .resizable()
            }
            .medium(.height(.s52), otherwise: .height(.s0))
            .large(.height(.s32))
            .large(.col(span: 2))
        }
        .rounded(.large)
    }
    .large(.padding(.bottom, .s0), otherwise: .small(.padding(.bottom, .s6)))
}

State variants

Hover and focus states? We got'em.

List {
    Section {
        VStack(spacing: .s4) {
            ForEach(projects) { project in
                Link(destination: project.url) {
                    HStack {
                        VStack(alignment: .leading) {
                            Text(project.title)
                                .accessibilityLabel("Title")
                                .fontWeight(.semibold)
                                .groupHover(.foregroundStyle(.white), otherwise: .foregroundStyle(.slate900))
                            Text(project.category)
                                .accessibilityLabel("Category")
                                .groupHover(.foregroundStyle(.blue200))
                        }
                        HStack(spacing: -.s1_5) {
                            ForEach(project.users) { user in
                                AsyncImage(url: user.avatar) { image in
                                    image
                                        .resizable()
                                } placeholder: {
                                    ProgressView()
                                }
                                .accessibilityLabel(user.name)
                                .scaledToFill()
                                .width(.s6)
                                .height(.s6)
                                .background(.slate100)
                                .rounded(.full)
                                .ring(.white, thickness: .t2, rounded: .full)
                            }
                        }
                        .accessibilityLabel("Users")
                        .justifyEnd()
                    }
                }
                .padding(.s3)
                .cursorPointer()
                .hover(.background(.blue500), otherwise: .background(.white))
                .rounded(.medium)
                .ring(.slate200, thickness: .t1, rounded: .medium)
                .shadow(.small)
                .group()
            }
            Link(destination: .init(string: "/new")!) {
                VStack {
                    Image(systemName: "plus")
                        .padding(.bottom, .s1)
                        .groupHover(.foregroundStyle(.blue500), otherwise: .foregroundStyle(.slate400))
                    Text("New Project")
                }
            }
            .width(.full)
            .padding(.vertical, .s3)
            .group()
            .hover { view in
                view
                    .border(.solid, .blue500, width: 2, rounded: .medium)
                    .background(.white)
                    .foregroundStyle(.blue500)
            } otherwise: { view in
                view
                    .border(.dashed, .slate300, width: 2, rounded: .medium)
            }
            .text(.small, leading: 6)
            .foregroundStyle(.slate900)
            .fontWeight(.medium)
            .cursorPointer()
        }
        .padding(.s4)
        .text(.small, leading: 6)
    } header: {
        VStack(spacing: .scale(.s4)) {
            HStack {
                Text("Projects")
                    .fontWeight(.semibold)
                    .foregroundStyle(.slate900)
                Spacer()
                Link(destination: .init(string: "/new")!) {
                    Image(systemName: "plus")
                        .padding(.trailing, .s2)
                    Text("New")
                }
                .padding(.leading, .s2)
                .padding(.trailing, .s3)
                .padding(.vertical, .s2)
                .hover(.background(.blue400), otherwise: .background(.blue500))
                .rounded(.medium)
                .foregroundStyle(.white)
                .text(.small)
                .fontWeight(.medium)
                .shadow(.small)
            }
            Form {
                HStack(spacing: 0) {
                    Image(systemName: "magnifyingglass")
                        .if(isFocused, then: { image in
                            image
                                .foregroundStyle(.blue500)
                        }, or: { image in
                            image
                                .dark(.foregroundStyle(.slate500), otherwise: .foregroundStyle(.slate400))
                        })
                        .padding(.leading, .s3)
                    TextField("", text: .constant(""), prompt: Text("Filter projects...")
                        .foregroundStyle(.slate400)
                    )
                    .focused($isFocused)
                    .textFieldStyle(.plain)
                    .height(.s6)
                    .foregroundStyle(.slate900)
                    .padding(.vertical, .s2)
                    .accessibilityLabel("Filter projects")
                }
                .rounded(.medium)
                .ring(isFocused: isFocused, focus: .blue500, thickness: .t2, otherwise: .slate200, thickness: .t1, rounded: .medium)
                .background(.white)
                .compositingGroup()
                .shadow(.small)
            }
        }
        .padding(.s4)
        .background(.white)
    }
    .listRowSeparator(.hidden)
}
.background(.slate50)
.foregroundStyle(.slate500)

Component-driven

Worried about duplication? Don’t be.

image

MoviesView.swift


import SwiftUI
import TailwindSwiftUI

struct MoviesView: View {
    var body: some View {
        VStack(spacing: 0) {
            Divide(.slate100) {
                NavigationView {
                    NavigationItem(url: .init(string: "/new")!, isActive: true) {
                        Text("New Releases")
                    }
                    NavigationItem(url: .init(string: "/top")!, isActive: false) {
                        Text("Top Rated")
                    }
                    NavigationItem(url: .init(string: "/picks")!, isActive: false) {
                        Text("Vincent's Picks")
                    }
                }
                ListView {
                    ForEach(movies, id: \.id) { movie in
                        ListItem(movie: movie)
                    }
                }
            }
        }
        .foregroundStyle(.slate500)
        .background(.white)
    }
}

NavigationView.swift


import SwiftUI

struct NavigationView<Content: View>: View {
    let content: Content
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: .s3) {
                content
            }
        }
        .padding(.all, .s6)
        .text(.small)
        .fontWeight(.medium)
    }
    
    init(@ViewBuilder _ content: () -> Content) {
        self.content = content()
    }
}

NavigationItem.swift


import SwiftUI

struct NavigationItem<Content: View>: View {
    let url: URL
    let isActive: Bool
    let content: Content
    
    var body: some View {
        Link(destination: url) {
            content
        }
        .padding(.horizontal, .s3)
        .padding(.vertical, .s2)
        .if(isActive) { view in
            view
                .background(.sky500)
                .foregroundStyle(.white)
        } or: { view in
            view
                .background(.slate50)
        }
        .rounded(.medium)
        .cursorPointer()
    }
    
    init(url: URL, isActive: Bool, @ViewBuilder _ content: () -> Content) {
        self.url = url
        self.isActive = isActive
        self.content = content()
    }
}

ListView.swift


import SwiftUI

struct ListView<Content: View>: View {
    let content: Content
    
    var body: some View {
        List {
            content
                .listRowSeparatorTint(.slate100)
        }
        .listStyle(.plain)
    }
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
}

ListItem.swift


import SwiftUI

struct ListItem: View {
    let movie: Movie
    
    var body: some View {
        HStack(spacing: .s6) {
            Image(movie.image)
                .resizable()
                .scaledToFill()
                .frame(width: 60, height: 88)
                .background(.slate100)
                .rounded(.medium)
            VStack(alignment: .leading, spacing: 0) {
                HStack {
                    Text(movie.title)
                        .font(.title2)
                        .fontWeight(.semibold)
                        .foregroundStyle(.slate900)
                        .padding(.trailing, .s20)
                    Spacer()
                    HStack(spacing: .s1) {
                        Image(systemName: "star.fill")
                            .foregroundStyle(.sky500)
                        Text(movie.starRating.description)
                    }
                }
                HStack(spacing: 0) {
                    Text(movie.rating)
                        .accessibilityLabel("Rating")
                        .padding(.s1_5)
                        .ring(.slate200, thickness: .t1, rounded: .notSpecific)
                    Text(movie.year.description)
                        .accessibilityLabel("Year")
                        .padding(.leading, .s2)
                    HStack(spacing: 0) {
                        Circle()
                            .frame(width: 2)
                            .padding(.horizontal, .s2)
                            .foregroundStyle(.slate300)
                            .accessibilityHidden(true)
                        Text(movie.genre)
                    }
                    .accessibilityLabel("Genre")
                    HStack(spacing: 0) {
                        Circle()
                            .frame(width: 2)
                            .padding(.horizontal, .s2)
                            .foregroundStyle(.slate300)
                            .accessibilityHidden(true)
                        Text(String(format: "%dh %dm", movie.runTime / 60, movie.runTime % 60))
                    }
                    .accessibilityLabel("Runtime")
                }
                .padding(.top, .s2)
                .text(.small, leading: 6)
                .fontWeight(.medium)
                Text(movie.cast)
                    .padding(.top, .s2)
                    .font(.body)
                    .accessibilityLabel("Cast")
                    .foregroundStyle(.slate400)
            }
            .frame(minWidth: 0)
        }
        .padding(.s6)
    }
}

Dark mode

Dark Mode.

Screenshot 2024-03-30 at 3 54 32 PM

Screenshot 2024-03-30 at 3 55 11 PM
VStack(spacing: 0) {
    VStack(spacing: .s6) {
        HStack(spacing: .s4) {
            Image("full-stack-radio.afb14e4e")
                .resizable()
                .scaledToFit()
                .frame(width: 88, height: 88)
                .rounded(.large)
                .background(.universalSlate100)
            VStack(alignment: .leading, spacing: .s1) {
                Group {
                    HStack(spacing: 0) {
                        Text("Ep.")
                            .accessibilityAddTraits(.isHeader)
                            .accessibilityLabel("Episode")
                        Text(" 128")
                    }
                    .dark(.foregroundStyle(.cyan400))
                    .foregroundStyle(.cyan500)
                    .text(.small)
                    Text("Scaling CSS at Heroku with Utility Classes")
                        .dark(.foregroundStyle(.slate400))
                        .foregroundStyle(.slate500)
                        .text(.small)
                    Text("Full Stack Radio")
                        .dark(.foregroundStyle(.slate50))
                        .foregroundStyle(.slate900)
                        .text(.large)
                }
                .padding(.vertical, 3)
            }
            .frame(minWidth: 0)
            .fontWeight(.semibold)
        }
        VStack(spacing: .s2) {
            ZStack {
                ProgressView(value: 1456, total: 4550)
                    .accessibilityLabel("music progress")
                    .height(.s2)
                    .overlay(alignment: .leading) {
                        Dark {
                            Color
                                .cyan400
                        } otherwise: {
                            Color.cyan500
                        }
                        .width(.half)
                    }
                    .dark(.background(.slate700))
                    .background(.slate100)
                    .rounded(.full)
                Dark {
                    Circle()
                        .fill(.cyan400)
                } otherwise: {
                    Circle()
                        .fill(.cyan500)
                }
                .width(.s1_5)
                .height(.s1_5)
                .ring(.slate900 / 5, thickness: .t1, inset: true)
                .width(.s4)
                .height(.s4)
                .background(.white)
                .rounded(.full)
                .ring(.cyan500, thickness: .t2, rounded: .full)
                .shadow()
            }
            HStack {
                Text("24:16")
                    .dark(.foregroundStyle(.slate100))
                    .foregroundStyle(.cyan500)
                Spacer()
                Text("75:50")
                    .dark(.foregroundStyle(.slate400))
                    .foregroundStyle(.slate500)
            }
            .text(.small, leading: 6)
            .fontWeight(.medium)
            .monospacedDigit()
        }
    }
    .padding(.horizontal, .s4)
    .padding(.top, .s4)
    .padding(.bottom, .s6)
    .dark(.background(.slate800), otherwise: .background(.white))
    .dark(.border(.slate500, .bottom), otherwise: .border(.slate100, .bottom))
    .rounded(.top, .extraLarge)
    HStack {
        HStack {
            Spacer()
            Image(systemName: "bookmark.fill")
                .accessibilityLabel("Add to favorites")
            Spacer()
            Button {} label: {
                Image(systemName: "gobackward")
                    .fontWeight(.bold)
            }
            .accessibilityLabel("Previous")
            Spacer()
        }
        Spacer()
            .width(.s20)
        HStack {
            Spacer()
            Button {} label: {
                Image(systemName: "goforward")
                    .fontWeight(.bold)
            }
            .accessibilityLabel("Skip 10 seconds")
            Spacer()
            Button {} label: {
                Text("1x")
                    .padding(.vertical, 3)
            }
            .padding(.horizontal, .s2)
            .dark(.background(.slate500), otherwise: .ring(.slate500, thickness: .t2, inset: true, rounded: .large))
            .rounded(.large)
            .dark(.foregroundStyle(.slate100))
            .foregroundStyle(.slate500)
            .text(.small, leading: 6)
            .fontWeight(.semibold)
            Spacer()
        }
    }
    .height(.s20)
    .padding(.vertical, -.s2)
    .dark(.background(.slate600), otherwise: .background(.slate50))
    .dark(.foregroundStyle(.slate200))
    .foregroundStyle(.slate500)
    .rounded(.bottom, .extraLarge)
    .buttonStyle(.plain)
    .overlay {
        Button {} label: {
            Image(systemName: "pause")
                .resizable()
                .frame(width: .point(30), height: .point(32))
        }
        .width(.s20)
        .height(.s20)
        .dark(.background(.slate100), otherwise: .background(.white))
        .dark(.foregroundStyle(.slate700))
        .foregroundStyle(.slate900)
        .rounded(.full)
        .ring(.slate900 / 5, thickness: .t1, rounded: .full)
        .shadow(.medium)
        .accessibilityLabel("Pause")
        .buttonStyle(.plain)
    }
}

Modern features

Cutting-edge is our comfort zone.

Grid

Screenshot 2024-03-30 at 7 27 36 PM
GridView(cols: 3, .gap(.s8)) {
    Group {
        Image("mountains-1")
            .resizable()
            .scaledToFill()
            .frame(height: 110)
        Image("mountains-2")
            .resizable()
            .scaledToFill()
            .frame(height: 110)
            .col(start: 2)
            .col(end: 4)
        Image("mountains-3")
            .resizable()
            .scaledToFill()
            .frame(height: 110)
        Image("mountains-4")
            .resizable()
            .scaledToFill()
            .frame(height: 110)
        Image("mountains-5")
            .resizable()
            .scaledToFill()
            .frame(height: 110)
    }
    .rounded(.large)
    .shadow(.large)
}

Transforms

Screenshot 2024-03-30 at 7 31 21 PM
GridView(cols: 3, .gap(.s8)) {
    Image("mountains-1")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
        .scaleEffect(1.1)
        .rotationEffect(.degrees(-6))
    Image("mountains-2")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .col(start: 2)
        .col(end: 4)
        .rounded(.large)
        .shadow(.large)
        .scaleEffect(0.75)
        .offset(x: 80, y: 16)
    Image("mountains-3")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
        .scaleEffect(x: 1.5, anchor: .trailing)
        .offset(y: 44)
    Image("mountains-4")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
        .offset(y: 96)
    Image("mountains-5")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
        .offset(x: 8, y: 60)
        .scaleEffect(0.75)
        .rotationEffect(.degrees(6))
}

Filters

Screenshot 2024-03-30 at 7 33 44 PM
GridView(cols: 3, .gap(.s8)) {
    Image("mountains-1")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
        .blur()
    Image("mountains-2")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .col(start: 2)
        .col(end: 4)
        .rounded(.large)
        .shadow(.large)
        .colorInvert()
    Image("mountains-3")
        .sepia()
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
    Image("mountains-4")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
        .grayscale()
    Image("mountains-5")
        .resizable()
        .scaledToFill()
        .frame(height: 110)
        .rounded(.large)
        .shadow(.large)
        .saturation(2)
}

Top categories

Loading Svelte Themes