Goals & prerequisites
A card communicates that several pieces of information belong to one subject. A surface provides a Material background and content-color context. Knowing the difference helps you build components that remain clear when their content, theme, or interactions change.
Complete layouts, state hoisting, and the Material 3 introduction first. This lesson teaches you to select a container, expose a useful component contract, apply theme roles, and give a card one understandable primary action. Its examples use androidx.compose.material3 throughout.
Choose the container by its job
Do not wrap every paragraph in a card. Use grouping when it helps someone understand the screen. A reading list may use plain list items separated by dividers, while a lesson overview can group its title, summary, and progress in one card.
| Component | Useful responsibility | Keep in mind |
|---|---|---|
Card | A filled container for one coherent subject | Its content uses a ColumnScope; add your own inner padding |
ElevatedCard | A card distinguished with elevation | Avoid relying on shadow alone to communicate selection |
OutlinedCard | A card distinguished with an outline | Outline and container colors should still come from the theme |
Surface | A shaped Material container and content-color context | Choose a layout inside when displaying several children |
ListItem | A conventional row with headline and optional supporting/trailing slots | It does not become an action merely because it resembles a row |
HorizontalDivider | A visual boundary between related regions | A divider is not a button or a substitute for a heading |
Slots & component contracts
A content slot is a composable lambda. It allows the caller to provide UI while the component provides structure. ListItem names its slots by purpose: headlineContent, supportingContent, leadingContent, and trailingContent. Those names are a useful guide to information hierarchy.
Your own card does not need a slot for every detail. Start with domain inputs such as title and completion state, plus an event such as onOpen. Add a content slot when callers truly need different content. Accept a default modifier and apply it once to the root so callers can place the entire component.
Color, shape & elevation
Theme colors have roles. Pair a container color with its matching “on” color so text remains legible across light and dark schemes. Material components expose content colors through composition locals; an ordinary Text inside a correctly configured surface can inherit that color. Explicit text colors override the inherited choice.
// Focused alternatives inside a composable; use material3 imports.
Card { Text("Filled card", Modifier.padding(16.dp)) }
ElevatedCard { Text("Elevated card", Modifier.padding(16.dp)) }
OutlinedCard { Text("Outlined card", Modifier.padding(16.dp)) }
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
shape = MaterialTheme.shapes.medium,
tonalElevation = 2.dp,
) {
Column(Modifier.padding(16.dp)) {
Text("Study reminder", style = MaterialTheme.typography.titleMedium)
Text("Continue with the next lesson.")
}
}tonalElevation and shadowElevation serve different visual purposes. Tonal elevation affects eligible surface coloring; shadow elevation controls the shadow. Neither is layout padding. Rounded corners belong to shape; adding a background modifier alone does not reproduce a Material component's color and interaction behavior.
Build a reusable lesson card
Create LessonCardDemo.kt in your app package, keep the package declaration, and add the following imports and declarations. Call LessonCardDemo() inside your generated app theme in setContent. No icon pack, image resource, or navigation dependency is needed.
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
fun LessonCard(
title: String,
description: String,
completed: Boolean,
onOpen: () -> Unit,
modifier: Modifier = Modifier,
) {
Card(
onClick = onOpen,
modifier = modifier,
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Text(description, style = MaterialTheme.typography.bodyMedium)
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
shape = MaterialTheme.shapes.small,
) {
Text(
text = if (completed) "Completed" else "Ready to study",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelLarge,
)
}
}
}
}
@Composable
fun LessonCardDemo(modifier: Modifier = Modifier) {
var opened by rememberSaveable { mutableStateOf(false) }
var completed by rememberSaveable { mutableStateOf(false) }
Column(
modifier = modifier.verticalScroll(rememberScrollState()).padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
LessonCard(
title = "Build reusable Compose components",
description = "Practice slots, theme colors and accessible actions.",
completed = completed,
onOpen = { opened = true },
modifier = Modifier.fillMaxWidth(),
)
Text(if (opened) "Lesson opened in this local demo." else "Tap the lesson card.")
TextButton(onClick = { completed = !completed }) {
Text(if (completed) "Mark as incomplete" else "Mark as complete")
}
HorizontalDivider()
ListItem(
headlineContent = { Text("Study time") },
supportingContent = { Text("Suggested reading and practice") },
trailingContent = { Text("30 min") },
)
}
}The parent owns completion and the opened message. LessonCard displays values and forwards an event; it does not invent a second completion state. The completion button sits outside the clickable card, so the two actions have separate targets. “Opened” is a local demonstration message; a real application can supply a navigation callback.
Choose one owner for the primary click
Use the clickable Card(onClick = ...) overload for an actionable card. A plain card is a container. Avoid adding a second clickable modifier to the same actionable root. Its built-in action already coordinates interaction indication, semantics, and enabled behavior.
A card may legitimately contain secondary actions, but design their meaning and targets explicitly. For a beginner component, keeping the whole-card action separate from secondary buttons is easier to understand and verify. Hoist events so a preview can supply an empty callback and a screen can supply navigation or another real action.
Flexible content is part of accessibility
Let descriptions wrap and cards grow vertically. A fixed height that fits one English title can cut off translated text or large fonts. The example truncates only the title after two lines; decide whether that is acceptable for your product and expose the full title on the destination screen. Never truncate essential warnings or instructions just to preserve symmetry.
Check TalkBack reading order and the card's announced action. A visible “Completed” label communicates more than a color change. Test both color schemes, long content, and increased font size. Keep decorative artwork out of the accessibility tree when its meaning is already present in text.
Diagnose common mistakes
| Symptom | Cause to inspect | Repair |
|---|---|---|
| Text touches the card edge | Card content has no inner padding | Put padding on an inner Column |
| Dark-mode text becomes hard to read | Hard-coded colors or mismatched container/content roles | Use paired theme colors and inspect inherited contentColor |
| One tap exposes two competing actions | Click handling exists at several overlapping levels | Give each intended action one deliberate target |
| A card ignores caller width | The incoming modifier was dropped or applied to a child | Forward it to the card root |
Key idea
Practice: add useful metadata
- Add an
estimatedMinutesinput to the reusable card. - Display the estimate below the summary using a supporting typography and color role.
- Verify a long title, a multi-line summary, dark mode, and large system text.
Expected result: the estimate changes when its caller changes, the entire card still opens once, and the completion action remains a separate target. Content grows without clipping.
Open the reference solution
// Extend LessonCard's parameters with:
estimatedMinutes: Int,
// Inside the padded Column, after the description, add:
Text(
text = "Estimated study time: $estimatedMinutes minutes",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
// At the call site, add:
estimatedMinutes = 30,
// For localization, use stringResource for labels and pluralStringResource
// for this quantity. Do not combine translated fragments with a number.Keep this metadata as an input. A card should not calculate study estimates by reading a repository during composition.
Recap & knowledge check
Key takeaways
- A card groups one subject; Surface supplies a Material container and content-color context.
- Add internal padding deliberately and forward caller modifiers to the root.
- Use theme color roles and distinguish tonal elevation from shadow elevation.
- Hoist state, expose clear events, and verify actions with assistive technology.
Quick check
1. Why does text touch the edge of a new Card?
2. A Surface uses secondaryContainer. Which content color is its natural pair?
3. Who should own completion state in the reusable LessonCard example?