Before creating a new issue, please confirm:
Which UI component?
Liveness
Gradle script dependencies
implementation("com.amplifyframework.ui:liveness:1.11.0")
Environment information
Gradle 8.13
Reproduced against liveness 1.11.0 (repo VERSION_NAME), Compose BOM 2026.03.00
Describe the bug
Six distinct UI elements in FaceLivenessDetector are routed through three shared MaterialTheme.colorScheme roles. background and onBackground in particular are applied to elements that are not app backdrops, which causes two problems: a visible defect in the shipped dark theme, and an inability to style any of those elements independently.
1. In dark theme the instruction chip blends into the backdrop.
The backdrop is hardcoded when no oval is displayed:
// FaceLivenessDetector.kt:237-243
val backgroundColor = if (livenessState.showingStartView) {
MaterialTheme.colorScheme.background
} else if (livenessState.faceGuideRect != null) {
Color.White
} else {
Color.Black
}
The non-actionable instruction chip — the "Connecting" and "Verifying" states — takes its container from the theme:
// InstructionMessage.kt:63-68
.background(
color = MaterialTheme.colorScheme.background,
shape = MaterialTheme.shapes.small
)
Both of those states occur while faceGuideRect is null, so the backdrop is Color.Black. With LivenessColorScheme.default() in dark mode, background is Color(0xFF0D1926). The chip therefore renders a near-black container on a pure black backdrop and has no visible boundary. The label itself stays legible (onBackground is white), so this is a loss of the chip's definition rather than unreadable text — but it happens with the SDK's own defaults and no customisation.
2. No element that uses these roles can be styled independently.
onBackground is the instruction chip's text in the idle/"Center your face", "Connecting" and "Verifying" states (InstructionMessage.kt:80, :107), and the RecordingIndicator "REC" label (RecordingIndicator.kt:80), and the CancelChallengeButton icon tint (CancelChallengeButton.kt:65).
background is the chip container (InstructionMessage.kt:65, :99), the REC chip (RecordingIndicator.kt:67), the cancel button (CancelChallengeButton.kt:59), the detector's root Surface (FaceLivenessDetector.kt:151), the start-view backdrop (:239) and the start-view FaceGuide scrim (:289). A different chip container therefore repaints the whole screen, so a dark chip on a light screen is not expressible at all.
primary is the active-instruction chip container (InstructionMessage.kt:101), the chip's spinner (:72), the camera-loading spinner (FaceLivenessDetector.kt:275), the progress bar fill (:424), and — via ButtonDefaults — the start-view button container (:315).
onPrimary is the active-instruction chip text and, via ButtonDefaults, that same button's label. It is only effectively dedicated when disableStartView = true.
Only error/onError (the too-close chip) and surface (the progress bar track) are each read in exactly one place.
Availability was not the constraint: inverseSurface, inverseOnSurface, surfaceVariant, onSurfaceVariant, secondary and their containers were all present in Material3 1.0.1, the version liveness originally shipped against. LivenessColorScheme also already sets ten roles, including onSurface, which is never read anywhere in the component.
Reproduction steps (if applicable)
- Run the liveness sample with the device in dark mode, using the default
MaterialTheme(colorScheme = LivenessColorScheme.default()).
- Start a check and observe the "Connecting" chip, then complete the challenge and observe the "Verifying" chip.
- Both render a
#0D1926 container against a Color.Black backdrop, so the chip has no visible edge.
Identified by code inspection; a visual confirmation of step 3 would be worth recording on the issue.
Code Snippet
// Setting a chip container colour is not possible without also
// repainting the root Surface, the REC badge and the cancel button,
// because all four read colorScheme.background.
MaterialTheme(
colorScheme = LivenessColorScheme.default().copy(
background = Color(0xFF303030)
)
) {
FaceLivenessDetector(/* ... */)
}
Log output
Additional information and screenshots
Suggested approach. Reassign the offending usages to semantically appropriate roles rather than adding a new theming API. inverseSurface/inverseOnSurface are Material's pairing for an overlay drawn over content, which is what the instruction chip is, and using paired roles preserves the contrast guarantee that independent per-element colours would lose.
| Element |
Today |
Proposed |
| Instruction chip (default / Connecting / Verifying) |
background / onBackground |
inverseSurface / inverseOnSurface |
| REC badge |
background / onBackground |
surfaceVariant / onSurfaceVariant |
| Cancel button |
background / onBackground |
secondaryContainer / onSecondaryContainer |
| Progress bar fill |
primary |
secondary |
| Instruction chip (active) |
primary / onPrimary |
unchanged |
| Instruction chip (too close) |
error / onError |
unchanged |
| Progress bar track |
surface |
unchanged |
Root Surface, start-view backdrop |
background |
unchanged — correct usage |
Compatibility. No API changes, so this is source- and binary-compatible; only rendered output changes, and only for consumers who supply their own ColorScheme without setting the newly-read roles. Updating LivenessColorScheme.default() to set the new roles to today's values leaves the documented path unaffected. ColorScheme has no notion of an unset value, so there is no way to fall back to background automatically for consumers who have not migrated — a release note plus a revert recipe (copy LivenessColorScheme.default() and set inverseSurface = background, inverseOnSurface = onBackground) is the practical mitigation. Compose Material3 ships equivalent role reassignments as bug fixes with a visual-change note, for example the chip and OutlinedButton colour corrections in 1.4.0 and the SurfaceContainer migration in 1.3.0.
Related. The styling branch for the actionable chip is selected by comparing rendered strings rather than by state (InstructionMessage.kt:88-92), so two states resolving to the same string cannot be styled differently and changing a string resource silently changes appearance. Keying on LivenessCheckState/FaceOvalPosition would remove that coupling and is worth doing alongside any of the above.
The hardcoded Color.White backdrop and FaceGuide default (FaceGuide.kt:44) used once the oval is displayed are likely deliberate, since a bright screen illuminates the subject, and are not proposed for change here.
Before creating a new issue, please confirm:
Which UI component?
Liveness
Gradle script dependencies
implementation("com.amplifyframework.ui:liveness:1.11.0")Environment information
Describe the bug
Six distinct UI elements in
FaceLivenessDetectorare routed through three sharedMaterialTheme.colorSchemeroles.backgroundandonBackgroundin particular are applied to elements that are not app backdrops, which causes two problems: a visible defect in the shipped dark theme, and an inability to style any of those elements independently.1. In dark theme the instruction chip blends into the backdrop.
The backdrop is hardcoded when no oval is displayed:
The non-actionable instruction chip — the "Connecting" and "Verifying" states — takes its container from the theme:
Both of those states occur while
faceGuideRectis null, so the backdrop isColor.Black. WithLivenessColorScheme.default()in dark mode,backgroundisColor(0xFF0D1926). The chip therefore renders a near-black container on a pure black backdrop and has no visible boundary. The label itself stays legible (onBackgroundis white), so this is a loss of the chip's definition rather than unreadable text — but it happens with the SDK's own defaults and no customisation.2. No element that uses these roles can be styled independently.
onBackgroundis the instruction chip's text in the idle/"Center your face", "Connecting" and "Verifying" states (InstructionMessage.kt:80,:107), and theRecordingIndicator"REC" label (RecordingIndicator.kt:80), and theCancelChallengeButtonicon tint (CancelChallengeButton.kt:65).backgroundis the chip container (InstructionMessage.kt:65,:99), the REC chip (RecordingIndicator.kt:67), the cancel button (CancelChallengeButton.kt:59), the detector's rootSurface(FaceLivenessDetector.kt:151), the start-view backdrop (:239) and the start-viewFaceGuidescrim (:289). A different chip container therefore repaints the whole screen, so a dark chip on a light screen is not expressible at all.primaryis the active-instruction chip container (InstructionMessage.kt:101), the chip's spinner (:72), the camera-loading spinner (FaceLivenessDetector.kt:275), the progress bar fill (:424), and — viaButtonDefaults— the start-view button container (:315).onPrimaryis the active-instruction chip text and, viaButtonDefaults, that same button's label. It is only effectively dedicated whendisableStartView = true.Only
error/onError(the too-close chip) andsurface(the progress bar track) are each read in exactly one place.Availability was not the constraint:
inverseSurface,inverseOnSurface,surfaceVariant,onSurfaceVariant,secondaryand their containers were all present in Material3 1.0.1, the version liveness originally shipped against.LivenessColorSchemealso already sets ten roles, includingonSurface, which is never read anywhere in the component.Reproduction steps (if applicable)
MaterialTheme(colorScheme = LivenessColorScheme.default()).#0D1926container against aColor.Blackbackdrop, so the chip has no visible edge.Identified by code inspection; a visual confirmation of step 3 would be worth recording on the issue.
Code Snippet
Log output
Additional information and screenshots
Suggested approach. Reassign the offending usages to semantically appropriate roles rather than adding a new theming API.
inverseSurface/inverseOnSurfaceare Material's pairing for an overlay drawn over content, which is what the instruction chip is, and using paired roles preserves the contrast guarantee that independent per-element colours would lose.background/onBackgroundinverseSurface/inverseOnSurfacebackground/onBackgroundsurfaceVariant/onSurfaceVariantbackground/onBackgroundsecondaryContainer/onSecondaryContainerprimarysecondaryprimary/onPrimaryerror/onErrorsurfaceSurface, start-view backdropbackgroundCompatibility. No API changes, so this is source- and binary-compatible; only rendered output changes, and only for consumers who supply their own
ColorSchemewithout setting the newly-read roles. UpdatingLivenessColorScheme.default()to set the new roles to today's values leaves the documented path unaffected.ColorSchemehas no notion of an unset value, so there is no way to fall back tobackgroundautomatically for consumers who have not migrated — a release note plus a revert recipe (copyLivenessColorScheme.default()and setinverseSurface = background,inverseOnSurface = onBackground) is the practical mitigation. Compose Material3 ships equivalent role reassignments as bug fixes with a visual-change note, for example the chip andOutlinedButtoncolour corrections in 1.4.0 and theSurfaceContainermigration in 1.3.0.Related. The styling branch for the actionable chip is selected by comparing rendered strings rather than by state (
InstructionMessage.kt:88-92), so two states resolving to the same string cannot be styled differently and changing a string resource silently changes appearance. Keying onLivenessCheckState/FaceOvalPositionwould remove that coupling and is worth doing alongside any of the above.The hardcoded
Color.Whitebackdrop andFaceGuidedefault (FaceGuide.kt:44) used once the oval is displayed are likely deliberate, since a bright screen illuminates the subject, and are not proposed for change here.