Differentiate Without Color
Color is a powerful design tool, but relying solely on color to convey information creates accessibility barriers for users with color blindness or low vision. In this post, we'll explore how to make your SwiftUI apps more accessible by ensuring that information is communicated through multiple visual channels, not just color alone.
What is Differentiate Without Color?
Differentiate Without Color is an accessibility feature that helps users who have difficulty distinguishing colors. Approximately 8% of men and 0.5% of women have some form of color vision deficiency, making it essential to provide alternative ways to convey information.
When users enable "Differentiate Without Color" in their accessibility settings, your app should supplement color-based information with additional visual indicators such as:
-
Icons and symbols
-
Text labels
-
Patterns and textures
-
Shapes and borders
-
Position and size differences
For example, instead of using only red and green to show error and success states, you should also include an X icon for errors and a checkmark for success.
How to Implement in SwiftUI
SwiftUI provides the accessibilityDifferentiateWithoutColor environment variable to detect when users have enabled this setting. You can use this to adapt your UI accordingly.
Checking the Environment Variable
import SwiftUI
struct ContentView: View {
@Environment(\.accessibilityDifferentiateWithoutColor)
var differentiateWithoutColor
var body: some View {
VStack {
if differentiateWithoutColor {
Text("Differentiate Without Color is enabled")
} else {
Text("Using standard color scheme")
}
}
}
}
Bad Practice: Color Only
Here's an example of what NOT to do - using color alone to convey status:
struct BadStatusView: View {
let isSuccess: Bool
var body: some View {
Text("Operation Complete")
.padding()
.background(isSuccess ? Color.green : Color.red)
.cornerRadius(8)
}
}
This design fails because color-blind users may not be able to distinguish between the red and green states.
Good Practice: Color + Icons
Here's a better approach that uses both color and icons:
struct GoodStatusView: View {
@Environment(\.accessibilityDifferentiateWithoutColor)
var differentiateWithoutColor
let isSuccess: Bool
var statusIcon: String {
isSuccess ? "checkmark.circle.fill" : "xmark.circle.fill"
}
var statusColor: Color {
isSuccess ? .green : .red
}
var body: some View {
HStack {
Image(systemName: statusIcon)
.foregroundColor(differentiateWithoutColor ? .primary : statusColor)
Text(isSuccess ? "Success" : "Error")
.foregroundColor(differentiateWithoutColor ? .primary : statusColor)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.stroke(differentiateWithoutColor ? Color.primary : statusColor, lineWidth: 2)
)
}
}
Chart Example with Patterns
When creating charts or graphs, use patterns in addition to colors:
struct AccessibleChartView: View {
@Environment(\.accessibilityDifferentiateWithoutColor)
var differentiateWithoutColor
let data: [(String, Double, String)] = [
("Category A", 0.3, "circle.fill"),
("Category B", 0.5, "square.fill"),
("Category C", 0.2, "triangle.fill")
]
var body: some View {
VStack(spacing: 20) {
ForEach(data, id: \.0) { item in
HStack {
// Icon helps differentiate
if differentiateWithoutColor {
Image(systemName: item.2)
.frame(width: 30)
}
Text(item.0)
.frame(width: 100, alignment: .leading)
// Visual bar
Rectangle()
.fill(colorForCategory(item.0))
.frame(width: CGFloat(item.1) * 200, height: 30)
.overlay(
// Add pattern when needed
differentiateWithoutColor ?
Image(systemName: item.2)
.resizable()
.scaledToFit()
.padding(4) : nil
)
}
}
}
.padding()
}
func colorForCategory(_ category: String) -> Color {
switch category {
case "Category A": return .blue
case "Category B": return .orange
case "Category C": return .purple
default: return .gray
}
}
}
Button States Example
For interactive elements like buttons, combine color with shape and text:
struct AccessibleButton: View {
@Environment(\.accessibilityDifferentiateWithoutColor)
var differentiateWithoutColor
let isActive: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
HStack {
// Always show text
Text(isActive ? "Active" : "Inactive")
// Add icon for clarity
Image(systemName: isActive ? "checkmark.circle" : "circle")
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.fill(isActive && !differentiateWithoutColor ?
Color.blue : Color.gray.opacity(0.2))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(isActive ? Color.primary : Color.gray,
lineWidth: differentiateWithoutColor ? 3 : 1)
)
)
}
}
}
Best Practices
-
Always provide multiple indicators: Never rely on color alone. Use icons, labels, patterns, or shapes alongside color.
-
Test with color blindness simulators: Use tools to see how your app appears to users with different types of color vision deficiency.
-
Use semantic colors: SwiftUI's semantic colors (
.primary,.secondary,.accentColor) adapt to user preferences automatically. -
Provide clear labels: Text labels should always accompany color-coded information.
-
Consider contrast: Even when adding icons, maintain sufficient color contrast for users with low vision.
-
Respect user preferences: Check the
accessibilityDifferentiateWithoutColorenvironment variable and adapt your UI accordingly.
Wrap up
Color alone is never enough to convey meaning — pair it with icons, labels, or patterns. The accessibilityDifferentiateWithoutColor environment variable makes it straightforward to adapt your UI when users have this preference enabled.
Resources:
Read more
- Subscripts in Swift • 2 minutes reading time.
- Safely unwrap optional values in SwiftUI bindings • 4 minutes reading time.
- Move your app to the background • 3 minutes reading time.
Share
Share Bluesky Mastodon Twitter LinkedIn Facebook