Your visionOS app gets the palette with the same code, and every token gives its dark stop, because visionOS reports a dark color scheme. Put that color where Apple recommends it on glass, in buttons and background layers, and leave plain text to the system's styles.
struct TripCard: View {
let theme = Color.proOrange
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Lisbon")
.font(.headline)
Text("3 nights from October 12")
.font(.subheadline)
.foregroundStyle(.secondary)
Text("Beach")
.font(.subheadline.bold())
.foregroundStyle(theme.foregroundPrimary) // _50 on visionOS
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(theme.backgroundTertiary) // _700 on visionOS
}
.padding()
.glassBackgroundEffect()
}
}The card stays on glass, the title and dates use the system's text styles, and the palette colors the tag, where its text and background are a token pair.
Why tokens give their dark stops#
visionOS has no light or dark appearance for your app to switch between: its glass adapts to the room behind the window instead. It still reports a dark color scheme, and Apple's own system colors use their dark values there. The package's Color(light:dark:), which every token in ColorTokens.swift is built with, reads that and picks the dark stop, and color functions work out their dark-mode result.
Keep windows on glass#
Apple asks apps to avoid solid colors on windows, because glass lets people see their surroundings and keeps contrast as the light changes. On visionOS, backgroundPrimary is _1000, an opaque near-black that would hide the room. Keep the window's glass, or .glassBackgroundEffect() for your own views, and give palette backgrounds to smaller areas: a tag, a button, a selected row.
Use system styles for text on glass#
Text on glass defaults to white, and the system's .primary, .secondary and .tertiary styles add vibrancy, which keeps text readable whatever shows through. A token is a solid color with a fixed contrast against glass, and the palette's contrast pairs hold between tokens, not against a room. So use tokens for text that sits on a token background, like the tag above.
Color a whole button#
Button {
// book the trip
} label: {
Text("Book now")
.font(.headline)
.foregroundStyle(theme.invertedForegroundPrimary) // _1000 on visionOS
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(theme.invertedBackgroundTertiary) // _250 on visionOS
}
.buttonStyle(.plain)Apple recommends putting color in a background layer or an entire button, so people can see it on any glass. A plain-style button keeps visionOS's standard hover effect, so it still brightens when people look at it. Building for interaction states covers the pressed and disabled colors.
UIKit on visionOS#
UIColor(light:dark:) builds for visionOS as it does for iOS, and gives the dark stop there. The code on the UIKit page works unchanged.
Sources#
- The library's Color+Dynamic.swift and UIColor+Dynamic.swift, which build tokens as dynamic
UIColors on visionOS, and Color+Adaptive.swift for the color functions. - Apple, WWDC23 session 10110, on bringing windowed apps to visionOS: "This platform reports its color scheme as dark", and plain buttons keep the standard hover effect.
- Apple, Design for spatial user interfaces (WWDC23), on glass, white text, vibrancy and where to use color.
- Apple Human Interface Guidelines, Materials: "visionOS doesn't have a distinct Dark Mode setting. Instead, glass automatically adapts to the luminance of the objects and colors behind it."
- Apple Human Interface Guidelines, Color: "visionOS system colors use the default dark color values."
See also
- Using ColorTokensKit in UIKit Give UIKit views the palette's tokens as UIColors that follow dark mode on every iOS version tested.
- Understanding semantic tokens Learn the 20 tokens by the job each one does, and get dark mode and passing contrast with them.
- Building for interaction states Give buttons and rows hover, pressed, selected and disabled colors that stay readable in both modes.
- How accessible is it? What the palette guarantees for contrast in every hue and mode, and what you still need to check.