- Published on
Adding Horizontal Padding to TextEditor in SwiftUI with contentMargins
- Authors

- Name
- Mick MacCallum
- @0x7fs
Adding padding inside a TextEditor has historically been one of the trickier layout challenges in SwiftUI. While you can easily add external padding using the .padding() modifier, adding internal padding that affects the text content without shifting the scrollbar required workarounds. With iOS 17, Apple introduced the contentMargins modifier that solves this problem elegantly.
The Problem with Traditional Padding
When you apply .padding() to a TextEditor, it adds space around the entire view, including the scrollbar. This creates unwanted spacing and doesn't give you the natural text inset behavior you'd expect from a text editor.
struct ContentView: View {
@State private var text = "Lorem ipsum dolor sit amet"
var body: some View {
TextEditor(text: $text)
.padding(.horizontal, 20) // This pushes the scrollbar inward
.border(.red)
}
}
The scrollbar gets pushed inward along with the text, which doesn't look right and wastes valuable screen space.
The Old Solution: UITextView Introspection
Before iOS 17, developers had to resort to accessing the underlying UITextView to modify its textContainerInset property. This typically required creating a custom view using UIViewRepresentable or using third-party libraries to introspect the view hierarchy.
// Pre-iOS 17 workaround - not recommended
extension UITextView {
open override func didMoveToWindow() {
super.didMoveToWindow()
self.textContainerInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
}
}
This approach had several downsides:
- Required accessing UIKit internals
- Global modifications could affect all TextEditor instances
- Fragile and could break with iOS updates
- More complex code that mixed UIKit and SwiftUI paradigms
The Modern Solution: contentMargins
iOS 17 introduced the contentMargins modifier specifically designed for this use case. Unlike padding, which adds space around a view, contentMargins adds space inside scrollable containers, affecting only the content while leaving scroll indicators in their natural position.
struct ContentView: View {
@State private var text = "Lorem ipsum dolor sit amet"
var body: some View {
TextEditor(text: $text)
.contentMargins(.horizontal, 20, for: .scrollContent)
.border(.red)
}
}
Understanding the Parameters
The contentMargins modifier takes three parameters:
- Edge set - Which edges to apply margins to (
.horizontal,.vertical,.all, or specific edges like.leading) - Length - The amount of spacing in points
- Placement (optional) - Where to apply the margins:
.scrollContent- Insets only the content (recommended for most cases).scrollIndicators- Insets only the scroll indicators.automatic- System decides (may create inconsistent spacing)
Why contentMargins is Better
The contentMargins modifier offers several advantages over previous approaches:
- Pure SwiftUI - No UIKit code required
- Scoped - Only affects the specific view you apply it to
- Precise control - Separate control over content and scroll indicators
- Works with all scrollable views - Also works with ScrollView, List, and other scrollable containers
- Forward compatible - Uses Apple's recommended API
Different Edge Configurations
You can apply margins to different edges based on your needs:
// Horizontal padding only
TextEditor(text: $text)
.contentMargins(.horizontal, 20, for: .scrollContent)
// Vertical padding only
TextEditor(text: $text)
.contentMargins(.vertical, 16, for: .scrollContent)
// All sides
TextEditor(text: $text)
.contentMargins(.all, 20, for: .scrollContent)
// Specific edges
TextEditor(text: $text)
.contentMargins(.leading, 20, for: .scrollContent)
.contentMargins(.trailing, 40, for: .scrollContent)
Combining with Other Modifiers
The contentMargins modifier works well with other TextEditor customizations:
struct ContentView: View {
@State private var text = "Start typing..."
var body: some View {
TextEditor(text: $text)
.contentMargins(.horizontal, 20, for: .scrollContent)
.contentMargins(.vertical, 16, for: .scrollContent)
.scrollContentBackground(.hidden)
.background(Color(.systemGray6))
.cornerRadius(8)
.padding()
}
}
Conditional Margins Based on Device
You can apply different margins based on device type or size class to create adaptive layouts:
struct ContentView: View {
@State private var text = "Start typing..."
@Environment(\.horizontalSizeClass) var sizeClass
var horizontalMargin: CGFloat {
sizeClass == .regular ? 40 : 16
}
var body: some View {
TextEditor(text: $text)
.contentMargins(.horizontal, horizontalMargin, for: .scrollContent)
}
}
Conclusion
The contentMargins modifier is the modern, SwiftUI-native way to add internal padding to TextEditor and other scrollable views. It provides precise control over content spacing without the complexity and fragility of UIKit workarounds. If you're targeting iOS 17 or later, this should be your go-to solution for TextEditor padding.
For apps that need to support iOS 16 or earlier, you'll still need to use the UITextView introspection approach, but for new projects, contentMargins offers a clean and elegant solution to a previously frustrating problem.
Continue Learning
Add Spacing to Toolbars with ToolbarSpacer in SwiftUI
Learn how to use ToolbarSpacer to add fixed and flexible spacing between toolbar items in SwiftUI, new in iOS 26.
Building a Floating Action Button (FAB) that Respects Keyboard in SwiftUI
Learn how to create a Material Design-style floating action button in SwiftUI that intelligently moves above the keyboard and avoids blocking text input fields.
Managing Keyboard Toolbar Items in SwiftUI (iOS 15+)
Learn how to add custom toolbar buttons above the keyboard in SwiftUI using the .toolbar modifier with keyboard placement for Done buttons, formatting controls, and more.
