Skip to content

[Liveness] Instruction chip, REC badge and cancel button all read background/onBackground: dark-theme contrast defect and no independent styling #333

Description

@mattcreaser

Before creating a new issue, please confirm:

  • I have searched for duplicate or closed issues.
  • I have read the guide for submitting bug reports.

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)

  1. Run the liveness sample with the device in dark mode, using the default MaterialTheme(colorScheme = LivenessColorScheme.default()).
  2. Start a check and observe the "Connecting" chip, then complete the challenge and observe the "Verifying" chip.
  3. 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

N/A - visual defect

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglivenessThis issue relates to the Liveness component

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions