# Migrating to Expressive

> From classic Material 3 APIs to their Expressive replacements, one component at a time.

- Interactive page: https://m3e.ljzapps.app/#foundations/migration

Expressive does not replace Material 3, it extends it. The classic APIs keep working, so you can migrate one screen or one component at a time. Most Expressive components are stable by now (button groups, split buttons, toolbars, the new navigation bar and rail, menus, chips). Some still need @OptIn(ExperimentalMaterial3ExpressiveApi::class), among them LoadingIndicator, the flexible app bars, the new list items and VerticalSlider. Everything ships in the Material3 alpha line, so pin the version.

## Theme

`MaterialTheme` → `MaterialExpressiveTheme`

Turns on the expressive motion scheme. Pass your color scheme yourself: without one it falls back to expressiveLightColorScheme(), so dark mode does not switch on its own. Nested MaterialExpressiveThemes inherit the values of the outer one.

Before:

```kotlin
MaterialTheme(colorScheme = scheme) { App() }
```

After:

```kotlin
MaterialExpressiveTheme(
    colorScheme = scheme,
    motionScheme = MotionScheme.expressive(),
) { App() }
```

## Buttons that react to touch

`Button(shape = ...), IconButton(shape = ...)` → `Button(shapes = ...), IconButton(shapes = ...)`

The press morph lives in a separate overload that takes ButtonShapes or IconButtonShapes. The old overload keeps working, it just never morphs.

Before:

```kotlin
Button(onClick = onSave) { Text("Save") }
```

After:

```kotlin
Button(onClick = onSave, shapes = ButtonDefaults.shapes()) { Text("Save") }
```

## Segmented buttons

`SingleChoiceSegmentedButtonRow + SegmentedButton` → `Row + ToggleButton with connected shapes`

The Expressive guidelines use connected button groups for selection. There is no dedicated composable: a Row with ConnectedSpaceBetween and ToggleButtons with the leading, middle and trailing shapes. Selection logic and the radio button role move into your code.

Before:

```kotlin
SingleChoiceSegmentedButtonRow {
    options.forEachIndexed { index, label ->
        SegmentedButton(
            selected = index == selected,
            onClick = { selected = index },
            shape = SegmentedButtonDefaults.itemShape(index, options.size),
        ) { Text(label) }
    }
}
```

After:

```kotlin
Row(horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween)) {
    options.forEachIndexed { index, label ->
        ToggleButton(
            checked = index == selected,
            onCheckedChange = { selected = index },
            shapes = when (index) {
                0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
                options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
                else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
            },
            modifier = Modifier.weight(1f).semantics { role = Role.RadioButton },
        ) { Text(label) }
    }
}
```

## Menus

`DropdownMenu + DropdownMenuItem` → `DropdownMenuPopup + DropdownMenuGroup + DropdownMenuItem with shapes`

DropdownMenu keeps the baseline look. The expressive menu splits sections into groups with a MenuDefaults.GroupSpacing gap, and items get a shape from MenuDefaults.itemShape(index, count). Choices use the selected or checked overloads instead of a custom trailing check.

Before:

```kotlin
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
    DropdownMenuItem(text = { Text("Copy") }, onClick = onCopy)
    DropdownMenuItem(text = { Text("Paste") }, onClick = onPaste)
}
```

After:

```kotlin
DropdownMenuPopup(expanded = expanded, onDismissRequest = { expanded = false }) {
    DropdownMenuGroup(shapes = MenuDefaults.groupShape(0, 1)) {
        DropdownMenuItem(onClick = onCopy, text = { Text("Copy") }, shape = MenuDefaults.itemShape(0, 2).shape)
        DropdownMenuItem(onClick = onPaste, text = { Text("Paste") }, shape = MenuDefaults.itemShape(1, 2).shape)
    }
}
```

## Filter and input chips

`FilterChip(shape = ...), InputChip(shape = ...)` → `FilterChip(shapes = FilterChipDefaults.shapes()), InputChip(shapes = InputChipDefaults.shapes())`

The shapes overload morphs the corners on select and press and switches to tonal colors. Migrate a whole row at once so the chips match. AssistChip and SuggestionChip have no such overload.

Before:

```kotlin
FilterChip(selected, onClick = { selected = !selected }, label = { Text("Vegan") })
```

After:

```kotlin
FilterChip(selected, onClick = { selected = !selected }, label = { Text("Vegan") }, shapes = FilterChipDefaults.shapes())
```

## Short waits

`CircularProgressIndicator()` → `LoadingIndicator()`

For indeterminate waits that should feel lively. Keep a progress indicator when the user needs to see how much is left.

Before:

```kotlin
CircularProgressIndicator()
```

After:

```kotlin
LoadingIndicator()
```

## Progress

`LinearProgressIndicator, CircularProgressIndicator` → `LinearWavyProgressIndicator, CircularWavyProgressIndicator`

Same progress lambda. The default wave is flat below 10 and above 95 percent progress.

Before:

```kotlin
LinearProgressIndicator(progress = { downloaded })
```

After:

```kotlin
LinearWavyProgressIndicator(progress = { downloaded })
```

## Navigation bar

`NavigationBar + NavigationBarItem` → `ShortNavigationBar + ShortNavigationBarItem`

Rename both. On medium widths, use iconPosition = NavigationItemIconPosition.Start and ShortNavigationBarArrangement.Centered.

Before:

```kotlin
NavigationBar {
    NavigationBarItem(selected, onClick, icon = { Icon(icon, null) }, label = { Text(label) })
}
```

After:

```kotlin
ShortNavigationBar {
    ShortNavigationBarItem(selected, onClick, icon = { Icon(icon, null) }, label = { Text(label) })
}
```

## Navigation rail

`NavigationRail + NavigationRailItem` → `WideNavigationRail + WideNavigationRailItem`

The rail can expand. Every item needs railExpanded, which you compute from the rail state, for example state.targetValue == WideNavigationRailValue.Expanded. ModalWideNavigationRail covers the modal case.

## Collapsing top app bars

`MediumTopAppBar, LargeTopAppBar` → `MediumFlexibleTopAppBar, LargeFlexibleTopAppBar`

Adds a subtitle slot and titleHorizontalAlignment. Collapsing still needs a scroll behavior plus Modifier.nestedScroll on the scrolling container.

Before:

```kotlin
LargeTopAppBar(title = { Text("Inbox") }, scrollBehavior = scrollBehavior)
```

After:

```kotlin
LargeFlexibleTopAppBar(
    title = { Text("Inbox") },
    subtitle = { Text("12 unread") },
    scrollBehavior = scrollBehavior,
)
```

## Speed dial

`A FAB plus your own animated column of small FABs` → `FloatingActionButtonMenu + ToggleFloatingActionButton`

The menu animates the items for you. Closing on item click, on back and on outside taps is still your code.

## Bottom app bar

`BottomAppBar` → `FlexibleBottomAppBar or HorizontalFloatingToolbar`

Expressive adds a docked toolbar (FlexibleBottomAppBar) and floating toolbars that can scroll away with FloatingToolbarDefaults.exitAlwaysScrollBehavior.

## Emphasized type

`MaterialTheme.typography.titleLarge.copy(fontWeight = ...)` → `MaterialTheme.typography.titleLargeEmphasized`

Every style of the type scale has an Emphasized twin. Use it for headlines and selected states instead of changing the weight by hand.
