Skip to content

Feedback

Feedback components communicate loading, transient status, and placeholder states.

Components

  • ProgressIndicator
  • Skeleton
  • SkeletonBlock
  • Toast
  • ToastHost
  • ChatLog
  • AssistantContextCard
  • AssistantQuickActionChips
  • AssistantStatusBanner
  • AssistantLimitPromptCard
  • AssistantDisclaimerFooter

Feedback components

Example

AppTheme {
    ProgressIndicator(
        progress = state.progress,
        label = "Syncing",
    )
}

Use progress = null for indeterminate loading. Toast is an in-composition visual component, not a platform android.widget.Toast wrapper.

Chat logs

ChatLog renders reusable conversation bubbles with user/assistant alignment, typing indicator state, and a retryable error bubble. Keep domain models in your app and map them into ChatMessage values at the UI boundary:

ChatLog(
    messages = state.turns.map { turn ->
        ChatMessage(
            id = turn.id,
            sender = ChatMessageSender.Assistant,
            state = ChatMessageState.Content(turn.text),
        )
    },
    idleHint = "Ask a question to get started.",
    typingDescription = "Assistant is responding",
    errorAction = ChatErrorAction(
        message = "Could not load the response.",
        retryLabel = "Retry",
        onRetry = onRetry,
    ),
)

Use ChatMessageState.Typing for in-flight responses and provide a localized typingDescription so the state is announced accessibly. For markdown or rich text, pass messageContent; the design-system component intentionally does not depend on a markdown renderer:

ChatLog(
    messages = messages,
    idleHint = "",
    errorAction = errorAction,
    messageContent = { _, text ->
        MarkdownText(markdown = text)
    },
)

Assistant chat surfaces

For quick-action assistant experiences, compose the chat log with the surrounding assistant components:

Column {
    AssistantStatusBanner(message = unavailableMessage)

    AssistantContextCard(
        title = contextTitle,
        highlight = contextHighlight,
        body = AssistantContextBody(text = sourceText, isQuoted = true),
        footnote = contextFootnote,
    )

    ChatLog(
        messages = messages,
        idleHint = idleHint,
        errorAction = errorAction,
        modifier = Modifier.weight(1f),
    )

    AssistantQuickActionChips(
        actions = actions,
        actionState = { action ->
            AssistantQuickActionState(
                isSelected = action in usedActions,
                isEnabled = serviceAvailable && !isResponding && action !in usedActions,
            )
        },
        label = { action -> action.label },
        onAction = onAction,
    )

    AssistantDisclaimerFooter(text = disclaimer)
}

AssistantQuickActionChips is generic over the action type, so apps can reuse it for one-shot actions, re-runnable actions, selected/in-flight actions, or disabled quota states. AssistantLimitPromptCard covers quota, upgrade, and gated-action prompts with caller-supplied copy and callbacks.

Skeleton loading placeholders

Skeleton renders a single full-width placeholder block sized to a minimum touch target height — a quick ghost row for simple loading states. For finer control (an explicit height, an optional fixed width, or a custom shape/color to mimic the real content it stands in for), use SkeletonBlock directly:

AppTheme {
    Column(verticalArrangement = Arrangement.spacedBy(AppTheme.spacing.x1)) {
        SkeletonBlock(height = 20.dp, width = 160.dp)
        SkeletonBlock(height = 16.dp)
    }
}

Both are decorative loading placeholders and are hidden from the accessibility tree (clearAndSetSemantics {}) since they carry no information a screen reader should stop on.

SkeletonBlock parameters

Parameter Type Description
height Dp Block height.
modifier Modifier Applied to the block.
width Dp? Fixed width; null expands to the constraints from modifier.
shape Shape Clip shape. Defaults to AppTheme.shapes.small.
background Color Placeholder color. Defaults to AppTheme.colors.surfaceContainer.

Showing toasts

Toast is only the visual; ToastHost is the driver. Hold a ToastHostState, place a ToastHost over your screen content, and call showToast from a coroutine. It shows one toast at a time, animates it in and out, and suspends until the toast is dismissed — returning how it ended (ToastResult).

Choose the lifetime per call with ToastDuration: Short/Long auto-dismiss after a timeout, while Indefinite keeps the toast until the user or caller dismisses it. An Indefinite toast with no action becomes tap-to-dismiss, so pass dismissContentDescription for TalkBack.

Anchor the toast with position = ToastPosition.Top or ToastPosition.Bottom (default). Pass an icon for a leading glyph; the icon is tinted to the surface color by default — pass iconTint = Color.Unspecified to keep a colorful icon's own colors.

val hostState = rememberToastHostState()
val scope = rememberCoroutineScope()

Box(Modifier.fillMaxSize()) {
    // …your screen content…

    PrimaryButton(
        text = "Save",
        onClick = {
            scope.launch {
                val result = hostState.showToast(
                    message = "Saved",
                    icon = AppTheme.icons.check,
                    actionLabel = "Undo",
                    duration = ToastDuration.Long,
                )
                if (result == ToastResult.ActionPerformed) {
                    // undo…
                }
            }
        },
    )

    ToastHost(
        hostState = hostState,
        position = ToastPosition.Bottom, // or ToastPosition.Top
        dismissContentDescription = "Dismiss",
    )
}