A utility-first Swift package packed with view modifiers that can be composed to build any design, directly in your SwiftUI code.
Core Concepts
Core Concepts
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.
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
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
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:
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:
.hover(_)
and .focus(_)
..hover(_ hoverTransform:)
.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))))
...
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
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
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
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
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
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
Simple
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
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
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
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
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.
Small
Medium
Large
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
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
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
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
Grid
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
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
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)
}