---
name: compose-components
description: "Build Material 3 UI components and custom composables with Jetpack Compose. Covers TopAppBar, Scaffold, BottomSheet, SnackbarHost, slot-based API patterns, MaterialTheme customization, dynamic color, custom color schemes, shape/typography/color token mapping, and modifier ordering best practices. Use when building Compose UI, creating custom components, setting up theming, or reviewing Compose code."
---

# Jetpack Compose Components

Material 3 components, custom composable patterns, theming, and modifier
best practices for Jetpack Compose targeting 2024-2025 standards with
Material 3 (androidx.compose.material3).

## Contents

- [Scaffold and TopAppBar](#scaffold-and-topappbar)
- [Bottom Sheets](#bottom-sheets)
- [SnackbarHost](#snackbarhost)
- [Slot-Based API Pattern](#slot-based-api-pattern)
- [Theming](#theming)
- [Dynamic Color](#dynamic-color)
- [Typography Tokens](#typography-tokens)
- [Shape Tokens](#shape-tokens)
- [Color Token Mapping](#color-token-mapping)
- [Modifier Ordering](#modifier-ordering)
- [Do's and Don'ts](#dos-and-donts)
- [Troubleshooting](#troubleshooting)
- [Review Checklist](#review-checklist)

## Scaffold and TopAppBar

Scaffold provides the standard Material 3 layout structure with slots for
top bar, bottom bar, FAB, snackbar, and content.

```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    onNavigateToSettings: () -> Unit,
    viewModel: HomeViewModel = hiltViewModel(),
) {
    val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
    val snackbarHostState = remember { SnackbarHostState() }

    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            TopAppBar(
                title = { Text("Home") },
                actions = {
                    IconButton(onClick = onNavigateToSettings) {
                        Icon(Icons.Default.Settings, contentDescription = "Settings")
                    }
                },
                scrollBehavior = scrollBehavior,
            )
        },
        snackbarHost = { SnackbarHost(snackbarHostState) },
        floatingActionButton = {
            FloatingActionButton(onClick = { /* action */ }) {
                Icon(Icons.Default.Add, contentDescription = "Add")
            }
        },
    ) { innerPadding ->
        HomeContent(
            modifier = Modifier.padding(innerPadding),
            viewModel = viewModel,
        )
    }
}
```

### TopAppBar Variants

| Variant | Behavior | Use For |
|---------|----------|---------|
| `TopAppBar` | Fixed or pinned | Simple screens |
| `MediumTopAppBar` | Collapses from medium to small | Detail screens |
| `LargeTopAppBar` | Collapses from large to small | Feature screens |
| `CenterAlignedTopAppBar` | Center-aligned title | Branded screens |

Always pair collapsible bars with `scrollBehavior` and `nestedScroll`:

```kotlin
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
```

## Bottom Sheets

### Modal Bottom Sheet

```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterSheet(
    onDismiss: () -> Unit,
    onApply: (FilterState) -> Unit,
) {
    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)

    ModalBottomSheet(
        onDismissRequest = onDismiss,
        sheetState = sheetState,
        dragHandle = { BottomSheetDefaults.DragHandle() },
    ) {
        FilterContent(
            onApply = { filter ->
                onApply(filter)
                onDismiss()
            },
        )
    }
}
```

### Standard Bottom Sheet (Non-modal)

```kotlin
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapWithSheet() {
    val scaffoldState = rememberBottomSheetScaffoldState()

    BottomSheetScaffold(
        scaffoldState = scaffoldState,
        sheetContent = { LocationDetails() },
        sheetPeekHeight = 128.dp,
    ) { innerPadding ->
        MapContent(modifier = Modifier.padding(innerPadding))
    }
}
```

## SnackbarHost

```kotlin
@Composable
fun ScreenWithSnackbar() {
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) },
    ) { innerPadding ->
        Button(
            onClick = {
                scope.launch {
                    val result = snackbarHostState.showSnackbar(
                        message = "Item deleted",
                        actionLabel = "Undo",
                        duration = SnackbarDuration.Short,
                    )
                    if (result == SnackbarResult.ActionPerformed) {
                        // Handle undo
                    }
                }
            },
            modifier = Modifier.padding(innerPadding),
        ) {
            Text("Delete")
        }
    }
}
```

## Slot-Based API Pattern

Design custom composables with slot parameters (lambdas returning `@Composable`
content). This follows the Material 3 pattern used by `Scaffold`, `Card`, etc.

```kotlin
@Composable
fun InfoCard(
    modifier: Modifier = Modifier,
    icon: @Composable () -> Unit,
    title: @Composable () -> Unit,
    subtitle: @Composable (() -> Unit)? = null,
    actions: @Composable RowScope.() -> Unit = {},
) {
    Card(modifier = modifier) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            icon()
            Spacer(modifier = Modifier.width(16.dp))
            Column(modifier = Modifier.weight(1f)) {
                ProvideTextStyle(MaterialTheme.typography.titleMedium) {
                    title()
                }
                subtitle?.let {
                    ProvideTextStyle(MaterialTheme.typography.bodyMedium) {
                        it()
                    }
                }
            }
            Row(content = actions)
        }
    }
}

// Usage
InfoCard(
    icon = { Icon(Icons.Default.Flight, contentDescription = null) },
    title = { Text("IST to JFK") },
    subtitle = { Text("Flight AB1234") },
    actions = {
        IconButton(onClick = { /* bookmark */ }) {
            Icon(Icons.Default.BookmarkBorder, contentDescription = "Bookmark")
        }
    },
)
```

### Slot Design Rules
- First parameter: `modifier: Modifier = Modifier`
- Required slots: non-nullable `@Composable` lambdas
- Optional slots: nullable with `= null` or provide default empty content
- Row-scoped slots: use `RowScope.() -> Unit` for weighted layouts
- Use `ProvideTextStyle` to set default typography for slot content

## Theming

### Custom MaterialTheme

```kotlin
@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit,
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content,
    )
}
```

### Custom Color Schemes

```kotlin
private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color.White,
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),
    secondary = Color(0xFF625B71),
    onSecondary = Color.White,
    secondaryContainer = Color(0xFFE8DEF8),
    onSecondaryContainer = Color(0xFF1D192B),
    surface = Color(0xFFFFFBFE),
    onSurface = Color(0xFF1C1B1F),
    error = Color(0xFFBA1A1A),
    onError = Color.White,
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    onPrimaryContainer = Color(0xFFEADDFF),
    surface = Color(0xFF1C1B1F),
    onSurface = Color(0xFFE6E1E5),
)
```

## Dynamic Color

Dynamic color (Material You) extracts colors from the user's wallpaper.
Available on Android 12+ (API 31).

```kotlin
val colorScheme = when {
    dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
        val context = LocalContext.current
        if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
    }
    darkTheme -> DarkColorScheme
    else -> LightColorScheme
}
```

Always provide static fallback schemes for devices below API 31.

## Typography Tokens

```kotlin
val AppTypography = Typography(
    displayLarge = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_bold)),
        fontWeight = FontWeight.Bold,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp,
    ),
    headlineMedium = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_medium)),
        fontWeight = FontWeight.Medium,
        fontSize = 28.sp,
        lineHeight = 36.sp,
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_regular)),
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp,
    ),
    labelLarge = TextStyle(
        fontFamily = FontFamily(Font(R.font.brand_medium)),
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp,
    ),
)

// Usage: always reference tokens, never raw values
Text(
    text = "Flight Details",
    style = MaterialTheme.typography.headlineMedium,
    color = MaterialTheme.colorScheme.onSurface,
)
```

## Shape Tokens

```kotlin
val AppShapes = Shapes(
    extraSmall = RoundedCornerShape(4.dp),
    small = RoundedCornerShape(8.dp),
    medium = RoundedCornerShape(12.dp),
    large = RoundedCornerShape(16.dp),
    extraLarge = RoundedCornerShape(28.dp),
)

// Usage
Card(shape = MaterialTheme.shapes.medium) { /* content */ }
```

## Color Token Mapping

| Design Token | Material 3 Property | Use For |
|---|---|---|
| `primary` | `colorScheme.primary` | Key actions, FAB, active states |
| `onPrimary` | `colorScheme.onPrimary` | Text/icons on primary |
| `primaryContainer` | `colorScheme.primaryContainer` | Subtle emphasis backgrounds |
| `surface` | `colorScheme.surface` | Card, sheet, dialog backgrounds |
| `surfaceVariant` | `colorScheme.surfaceVariant` | Alternate surface (chips, text fields) |
| `error` | `colorScheme.error` | Error states and destructive actions |
| `outline` | `colorScheme.outline` | Borders, dividers |
| `outlineVariant` | `colorScheme.outlineVariant` | Subtle borders |

## Modifier Ordering

Modifier order matters. Modifiers are applied outside-in, so the order
determines layout, clipping, and gesture behavior.

```kotlin
// CORRECT: click area includes padding, background is behind content
Box(
    modifier = Modifier
        .fillMaxWidth()
        .clip(MaterialTheme.shapes.medium)   // clip first
        .background(MaterialTheme.colorScheme.surface)
        .clickable { /* action */ }           // clickable after background
        .padding(16.dp)                       // padding inside clickable area
)

// WRONG: padding before clickable = dead tap zone around content
Box(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)                       // padding OUTSIDE clickable
        .clickable { /* action */ }           // only inner area is tappable
)
```

### Recommended Modifier Order

1. Layout constraints: `fillMaxWidth`, `size`, `weight`
2. Drawing: `clip`, `shadow`, `background`, `border`
3. Interaction: `clickable`, `toggleable`, `selectable`
4. Padding: `padding` (inner padding after interaction)
5. Semantics: `semantics`, `testTag`, `contentDescription`

## Do's and Don'ts

### Do's
- Always pass `modifier: Modifier = Modifier` as the first parameter
- Use `MaterialTheme.colorScheme` / `typography` / `shapes` for all tokens
- Provide content descriptions for all interactive icons
- Use `ProvideTextStyle` inside slot-based components
- Apply `innerPadding` from Scaffold to content
- Use `remember` for expensive objects in composition

### Don'ts
- Do not hardcode colors: `Color(0xFF...)` in composables -- use theme tokens
- Do not hardcode text sizes: `fontSize = 14.sp` -- use typography tokens
- Do not hardcode corner radii: `RoundedCornerShape(12.dp)` -- use shape tokens
- Do not nest `Scaffold` inside `Scaffold`
- Do not use `material` (M2) and `material3` (M3) in the same screen
- Do not forget `Modifier.nestedScroll` when using collapsible TopAppBar
- Do not create new `SnackbarHostState` inside recomposition (use `remember`)
- Do not skip `contentDescription` on icons (accessibility violation)

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| TopAppBar does not collapse | Missing `nestedScroll` modifier on Scaffold | Add `Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)` |
| Bottom sheet appears behind content | Not using `ModalBottomSheet` at top level | Place sheet outside Scaffold or at the screen root |
| Snackbar never appears | `SnackbarHostState` not passed to `Scaffold` | Pass `snackbarHost = { SnackbarHost(state) }` |
| Theme colors not applied | Using `material` instead of `material3` imports | Replace `androidx.compose.material.*` with `material3.*` |
| Dynamic color crashes on API < 31 | No version check | Guard with `Build.VERSION.SDK_INT >= Build.VERSION_CODES.S` |
| Ripple not visible on custom component | Missing `clickable` or `indication` | Add `.clickable(interactionSource, indication)` |
| Text style overridden by parent | Conflicting `ProvideTextStyle` | Use `MaterialTheme.typography` reference directly |

## Review Checklist

- [ ] All colors use `MaterialTheme.colorScheme.*` -- no hardcoded hex
- [ ] All text uses `MaterialTheme.typography.*` -- no raw `fontSize`
- [ ] All shapes use `MaterialTheme.shapes.*` -- no inline `RoundedCornerShape`
- [ ] `modifier: Modifier = Modifier` is the first parameter on custom composables
- [ ] Icons have `contentDescription` (null only when decorative with adjacent label)
- [ ] `Scaffold` applies `innerPadding` to content
- [ ] `SnackbarHostState` is `remember`ed
- [ ] Collapsible TopAppBar has `nestedScroll` modifier
- [ ] Dynamic color has API 31 guard with static fallback
- [ ] No mixing of M2 and M3 imports in the same screen
