Slot APIs

You've already used a slot API without necessarily naming it that: Scaffold(topBar = { ... }, content = { ... }) doesn't take a giant list of configuration flags describing what the top bar should look like — it takes an actual composable lambda and simply places it where the top bar goes. That's the slot pattern: instead of a composable exposing dozens of parameters trying to anticipate every possible customization (title: String, titleColor: Color, showIcon: Boolean, iconTint: Color...), it exposes a "slot" — a parameter typed @Composable () -> Unit — and lets the caller put whatever composable content they want into that position.

Why this is genuinely a bit advanced: Slot APIs are simple to use but take some practice to design well — deciding which parts of a composable should be configuration parameters versus open slots is a real API-design judgment call, not a mechanical rule.

The Simplest Slot: a content Lambda

The most basic slot is a single content: @Composable () -> Unit parameter, which is exactly what Column, Row, Box, and Card all accept as their trailing lambda:

Card(modifier = Modifier.padding(8.dp)) {
    // everything in here is the "content" slot — Card has no idea what's inside
    Column(modifier = Modifier.padding(16.dp)) {
        Text("Blueberry Pancakes")
        Text("25 min")
    }
}

Card only knows about styling — elevation, shape, background color. It has zero knowledge of Text, Column, or anything else you put inside it, and that's exactly why the same Card composable can wrap a simple label in one screen and a whole complex form in another.

Named Slots: Multiple Composable Parameters

A composable isn't limited to one slot. Scaffold exposes several named slots — topBar, bottomBar, floatingActionButton, content — each a separate @Composable lambda parameter, so the caller can fill in exactly the regions relevant to their screen and leave the rest at their defaults:

RecipeListScreen.kt — using Scaffold's named slots

@Composable
fun RecipeListScreen(recipes: List, onAddClick: () -> Unit) {
    Scaffold(
        topBar = { TopAppBar(title = { Text("RecipeBox") }) },
        floatingActionButton = {
            FloatingActionButton(onClick = onAddClick) {
                Icon(Icons.Default.Add, contentDescription = "Add recipe")
            }
        }
    ) { padding ->
        LazyColumn(modifier = Modifier.padding(padding)) {
            items(recipes) { recipe -> RecipeRow(recipe) }
        }
    }
}

Note that the main content slot here receives a parameter of its own — PaddingValues — so the caller knows how much space the top bar and any system bars already occupy. A slot lambda can carry data outward like this just as easily as it carries composables inward.

Worked Example: RecipeCardContainer

Suppose several RecipeBox screens want a card with a consistent header area (icon, title, maybe a favorite button) above completely different body content — one screen shows ingredients, another shows a photo, another shows a short summary. A single composable with a giant parameter list trying to cover every case would be unmanageable. A two-slot composable expresses this cleanly:

RecipeCardContainer.kt — a reusable two-slot card

@Composable
fun RecipeCardContainer(
    modifier: Modifier = Modifier,
    header: @Composable () -> Unit,
    content: @Composable () -> Unit
) {
    Card(modifier = modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(16.dp)) {
            header()
            Spacer(modifier = Modifier.height(8.dp))
            content()
        }
    }
}

RecipeCardContainer itself knows nothing about recipes, ingredients, or photos — it only knows "put the header slot, then a gap, then the content slot." Every screen fills those slots differently:

Two very different uses of the same container

// Recipe list row: compact header + ingredient count
RecipeCardContainer(
    header = {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Icon(Icons.Default.Restaurant, contentDescription = null)
            Spacer(modifier = Modifier.width(8.dp))
            Text(recipe.title, style = MaterialTheme.typography.titleMedium)
        }
    },
    content = { Text("${recipe.ingredients.size} ingredients") }
)

// Recipe detail screen: header with a favorite toggle + full steps list
RecipeCardContainer(
    header = {
        Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
            Text(recipe.title, style = MaterialTheme.typography.headlineSmall)
            IconButton(onClick = onToggleFavorite) { Icon(Icons.Default.FavoriteBorder, contentDescription = null) }
        }
    },
    content = {
        recipe.steps.forEachIndexed { index, step -> Text("${index + 1}. $step") }
    }
)

This is composition over configuration in practice: instead of RecipeCardContainer trying to grow parameters to cover every future screen's needs, it stays small and stable, and all the variation lives in the composables passed into its slots.

Slots Can Be Optional

A slot parameter can default to an empty lambda, making it fully optional for callers who don't need that region at all — the same way Scaffold's bottomBar defaults to doing nothing if you don't supply one:

@Composable
fun RecipeCardContainer(
    modifier: Modifier = Modifier,
    header: @Composable () -> Unit = {},
    content: @Composable () -> Unit
) { /* ... */ }
Next up: Slots let callers supply whole composables. Building Custom Modifiers covers the equivalent idea for styling and behavior — writing your own reusable Modifier extension.