Guides
Theming
The style seam, the six built-in themes, building a palette of your own, and why the native keyboard's backdrop has to stay clear.
Every colour and metric the keyboard draws comes from a KeyboardStyleProvider. The default,
NativeKeyboardStyle, reproduces the measured native look in light and dark. You override only
what you want; the metrics and fonts keep their native proportions unless you replace those too.
The values inside NativeKeyboardStyle and NativeMetrics are the engine’s own — read them
through the API rather than copying numbers out of it, so a build that ships a newer measured
snapshot updates your keyboard along with everyone else’s.
The protocol
public protocol KeyboardStyleProvider: Sendable {
var metrics: NativeMetrics { get }
func backgroundColor() -> KeyboardColor
func keyFill(for role: KeyRole) -> KeyboardColor
func keyFillOnGlass(for role: KeyRole) -> KeyboardColor
func keyFillPressed(for role: KeyRole) -> KeyboardColor
func keyTextColor(for role: KeyRole) -> KeyboardColor
func keyFontSize(for action: KeyboardAction) -> CGFloat
func accentFill() -> KeyboardColor
func accentTextColor() -> KeyboardColor
}
Several of those have defaults, so a minimal provider implements three:
keyFontSize(for:)returns the native size for the action.keyFillOnGlass(for:)falls back tokeyFill(for:).keyFillPressed(for:)falls back tokeyFill(for:), which means a custom style that does not override it simply has no pressed state on the keys that have no key-pop of their own — backspace, return, space, emoji,123, globe, shift.accentFill()andaccentTextColor()derive from the rest.
KeyRole is .standard (letters and the space bar) or .control (shift, backspace, 123,
globe, and the rest of the chrome). KeyboardColor carries a light and a dark RGBA, and
RGBA carries alpha, which is how translucency is expressed.
public struct KeyboardColor: Sendable, Equatable {
public var light: RGBA
public var dark: RGBA
}
public struct RGBA: Sendable, Equatable {
public init(_ red: Double, _ green: Double, _ blue: Double, _ alpha: Double = 1)
}
The native style
NativeKeyboardStyle() // measured iOS 26 colours, iPhone portrait
NativeKeyboardStyle(metrics: .iPhoneLandscape) // the landscape metrics
NativeMetrics carries the measured geometry — row heights, spacing, key shape, type sizes — plus
the two derived values a host actually needs: keyAreaHeight(rowCount:) for sizing the input view,
and popOverflow(for:) for the headroom a given key-pop style needs above the top row. Ask it for
those rather than hardcoding a height; they change with the design snapshot.
The built-in themes
KeyboardBehavior.theme selects one of six. .native is the system look and the default; the
others recolour the entire keyboard — keys, text, accent, bubbles, callouts and the suggestion bar
— from a single palette.
public enum KeyboardTheme: String, Sendable, Equatable, Codable, CaseIterable {
case native, midnight, ocean, sunset, forest, graphite
}
Each case has a displayName for a picker, a palette, and resolves to a provider:
let style = behavior.theme.style() // any KeyboardStyleProvider
A non-native theme is a solid look, so it opts out of the translucent backdrop — glass would show
the app through the very background the theme just painted. PressboardRootView forces
translucentBackground off whenever the theme is not .native.
A palette of your own
For a recolour, build a ThemePalette and hand it to ThemedKeyboardStyle rather than
implementing the protocol from scratch. You get every derived colour — callouts, the pop, the
bar — for free.
// Six colours are the whole of a theme. These are placeholders — put your brand here.
let palette = ThemePalette(
background: KeyboardColor(light: RGBA(0.96, 0.93, 0.88), dark: RGBA(0.13, 0.10, 0.16)),
standardKey: KeyboardColor(light: RGBA(1.00, 0.99, 0.97), dark: RGBA(0.24, 0.20, 0.30)),
controlKey: KeyboardColor(light: RGBA(0.86, 0.81, 0.74), dark: RGBA(0.18, 0.15, 0.23)),
keyText: KeyboardColor(light: RGBA(0.11, 0.09, 0.07), dark: RGBA(0.97, 0.96, 0.99)),
accent: KeyboardColor(light: RGBA(0.78, 0.30, 0.16), dark: RGBA(0.94, 0.47, 0.29)),
accentText: KeyboardColor(light: RGBA(1, 1, 1), dark: RGBA(0.10, 0.06, 0.04)))
let style = ThemedKeyboardStyle(palette: palette)
For the native look, do not build a palette at all — NativeKeyboardStyle() is the measured one,
and it tracks the design snapshot the running system matches.
For anything beyond a recolour, implement the protocol:
struct BrandStyle: KeyboardStyleProvider {
// Keep the native geometry; only the paint is yours.
var metrics: NativeMetrics = .iPhonePortrait
func backgroundColor() -> KeyboardColor {
KeyboardColor(light: RGBA(0.96, 0.93, 0.88), dark: RGBA(0.13, 0.10, 0.16))
}
func keyFill(for role: KeyRole) -> KeyboardColor {
role == .standard
? KeyboardColor(light: RGBA(1.00, 0.99, 0.97), dark: RGBA(0.24, 0.20, 0.30))
: KeyboardColor(light: RGBA(0.86, 0.81, 0.74), dark: RGBA(0.18, 0.15, 0.23))
}
func keyTextColor(for role: KeyRole) -> KeyboardColor {
KeyboardColor(light: RGBA(0.11, 0.09, 0.07), dark: RGBA(0.97, 0.96, 0.99))
}
// Optional, but the difference between blending with the app and sitting on top of it.
// The alpha is what does the work here: a translucent key reads as part of the system's
// glass, an opaque one reads as a block laid over it.
func keyFillOnGlass(for role: KeyRole) -> KeyboardColor {
role == .standard
? KeyboardColor(light: RGBA(1, 1, 1, 0.92), dark: RGBA(1, 1, 1, 0.16))
: KeyboardColor(light: RGBA(1, 1, 1, 0.62), dark: RGBA(1, 1, 1, 0.06))
}
}
Pass it to the view as style:, or resolve it from your own theme type.
Translucency and liquid glass
The iOS 26 keyboard is not an opaque rectangle. It floats on the system’s translucent backdrop and
blends with the app behind it. translucentBackground is on by default, and reproducing it takes
two cooperating pieces — one in the engine, one in your extension.
The engine half
With translucentBackground on, the keyboard’s own background is drawn fully clear. A
keyboard extension already renders over the system’s translucent backdrop, so any material or
colour fill of our own only adds a grey veil, and a grey veil reads as an opaque rectangle. Clear
lets the real system glass through.
The keys are translucent on that glass, through keyFillOnGlass, so they blend rather than
sitting on the surface as darker blocks. With translucentBackground off, keys use the opaque
keyFill and the background is solid. Both sets of values are measured; a custom style supplies
its own.
Your half
The glass only shows if nothing behind the SwiftUI view is opaque:
host.view.backgroundColor = .clear // the UIHostingController's view
view.backgroundColor = .clear // the input view controller's own view
host.view.clipsToBounds = false
view.clipsToBounds = false
PressboardInputViewController does this for you. A hand-built controller that skips it puts the
default opaque view in front of the system backdrop, and you are back to a rectangle even though
the engine is drawing clear.
Why not a visual effect view
A custom keyboard is not handed the host app’s rendered content, so a UIVisualEffectView or
.regularMaterial has nothing to blur. It paints a flat grey veil instead. The correct approach
is to clear everything and rely on the system-provided backdrop — which is what the native
keyboard does.
Key-pop headroom
The character preview for the top key row extends above the keyboard. An extension cannot draw
outside its frame; the system clips it, and un-clipping the system’s container views does not
help. Instead, reserve the overflow as headroom inside a slightly taller keyboard: pass
PressboardKeyboardView(topInset:) the value
max(0, style.metrics.popOverflow(for: behavior.characterPreviewStyle) - toolbarHeight)
and add the same amount to your input view’s height. The inset is filled by the translucent
backdrop, so it reads as a marginally taller keyboard rather than a gap. The controller exposes
this as popTopInset and PressboardRootView passes it through.
Design snapshots
Parity is measured against a specific iOS release, and Apple redraws the keyboard between
releases. nativeDesignID selects which measured snapshot to render.
nil, the default, is automatic: the newest snapshot measured on an iOS no newer than the
device’s. Pin an id only to hold one look deliberately; an id a build does not ship resolves back
to automatic rather than failing.
Two smaller appearance switches
uniformKeyColorfills every key, including shift, backspace, return, globe, mic and123, with the letter-key colour instead of the darker native control colour. Off is native; it is on in.pressboardDefault.showSpaceLabelshows the word “space” on the space bar. Turn it off to leave it blank — for example when you want only a language indicator in the corner, supplied through the view’skeyOverlayslot.
Next: Layouts and languages.